rustybuzz/hb/
common.rs

1use alloc::string::String;
2use core::ops::{Bound, RangeBounds};
3
4use ttf_parser::Tag;
5
6use super::text_parser::TextParser;
7
8pub type hb_codepoint_t = char; // uint32_t in C++
9
10/// Defines the direction in which text is to be read.
11#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
12pub enum Direction {
13    /// Initial, unset direction.
14    Invalid,
15    /// Text is set horizontally from left to right.
16    LeftToRight,
17    /// Text is set horizontally from right to left.
18    RightToLeft,
19    /// Text is set vertically from top to bottom.
20    TopToBottom,
21    /// Text is set vertically from bottom to top.
22    BottomToTop,
23}
24
25impl Direction {
26    #[inline]
27    pub(crate) fn is_horizontal(self) -> bool {
28        match self {
29            Direction::Invalid => false,
30            Direction::LeftToRight => true,
31            Direction::RightToLeft => true,
32            Direction::TopToBottom => false,
33            Direction::BottomToTop => false,
34        }
35    }
36
37    #[inline]
38    pub(crate) fn is_vertical(self) -> bool {
39        !self.is_horizontal()
40    }
41
42    #[inline]
43    pub(crate) fn is_forward(self) -> bool {
44        match self {
45            Direction::Invalid => false,
46            Direction::LeftToRight => true,
47            Direction::RightToLeft => false,
48            Direction::TopToBottom => true,
49            Direction::BottomToTop => false,
50        }
51    }
52
53    #[inline]
54    pub(crate) fn is_backward(self) -> bool {
55        !self.is_forward()
56    }
57
58    #[inline]
59    pub(crate) fn reverse(self) -> Self {
60        match self {
61            Direction::Invalid => Direction::Invalid,
62            Direction::LeftToRight => Direction::RightToLeft,
63            Direction::RightToLeft => Direction::LeftToRight,
64            Direction::TopToBottom => Direction::BottomToTop,
65            Direction::BottomToTop => Direction::TopToBottom,
66        }
67    }
68
69    pub(crate) fn from_script(script: Script) -> Option<Self> {
70        // https://docs.google.com/spreadsheets/d/1Y90M0Ie3MUJ6UVCRDOypOtijlMDLNNyyLk36T6iMu0o
71
72        match script {
73            // Unicode-1.1 additions
74            script::ARABIC |
75            script::HEBREW |
76
77            // Unicode-3.0 additions
78            script::SYRIAC |
79            script::THAANA |
80
81            // Unicode-4.0 additions
82            script::CYPRIOT |
83
84            // Unicode-4.1 additions
85            script::KHAROSHTHI |
86
87            // Unicode-5.0 additions
88            script::PHOENICIAN |
89            script::NKO |
90
91            // Unicode-5.1 additions
92            script::LYDIAN |
93
94            // Unicode-5.2 additions
95            script::AVESTAN |
96            script::IMPERIAL_ARAMAIC |
97            script::INSCRIPTIONAL_PAHLAVI |
98            script::INSCRIPTIONAL_PARTHIAN |
99            script::OLD_SOUTH_ARABIAN |
100            script::OLD_TURKIC |
101            script::SAMARITAN |
102
103            // Unicode-6.0 additions
104            script::MANDAIC |
105
106            // Unicode-6.1 additions
107            script::MEROITIC_CURSIVE |
108            script::MEROITIC_HIEROGLYPHS |
109
110            // Unicode-7.0 additions
111            script::MANICHAEAN |
112            script::MENDE_KIKAKUI |
113            script::NABATAEAN |
114            script::OLD_NORTH_ARABIAN |
115            script::PALMYRENE |
116            script::PSALTER_PAHLAVI |
117
118            // Unicode-8.0 additions
119            script::HATRAN |
120
121            // Unicode-9.0 additions
122            script::ADLAM |
123
124            // Unicode-11.0 additions
125            script::HANIFI_ROHINGYA |
126            script::OLD_SOGDIAN |
127            script::SOGDIAN |
128
129            // Unicode-12.0 additions
130            script::ELYMAIC |
131
132            // Unicode-13.0 additions
133            script::CHORASMIAN |
134            script::YEZIDI |
135
136            // Unicode-14.0 additions
137            script::OLD_UYGHUR => {
138                Some(Direction::RightToLeft)
139            }
140
141            // https://github.com/harfbuzz/harfbuzz/issues/1000
142            script::OLD_HUNGARIAN |
143            script::OLD_ITALIC |
144            script::RUNIC => {
145                None
146            }
147
148            _ => Some(Direction::LeftToRight),
149        }
150    }
151}
152
153impl Default for Direction {
154    #[inline]
155    fn default() -> Self {
156        Direction::Invalid
157    }
158}
159
160impl core::str::FromStr for Direction {
161    type Err = &'static str;
162
163    fn from_str(s: &str) -> Result<Self, Self::Err> {
164        if s.is_empty() {
165            return Err("invalid direction");
166        }
167
168        // harfbuzz also matches only the first letter.
169        match s.as_bytes()[0].to_ascii_lowercase() {
170            b'l' => Ok(Direction::LeftToRight),
171            b'r' => Ok(Direction::RightToLeft),
172            b't' => Ok(Direction::TopToBottom),
173            b'b' => Ok(Direction::BottomToTop),
174            _ => Err("invalid direction"),
175        }
176    }
177}
178
179/// A script language.
180#[derive(Clone, PartialEq, Eq, Hash, Debug)]
181pub struct Language(String);
182
183impl Language {
184    /// Returns the language as a string.
185    #[inline]
186    pub fn as_str(&self) -> &str {
187        self.0.as_str()
188    }
189}
190
191impl core::str::FromStr for Language {
192    type Err = &'static str;
193
194    fn from_str(s: &str) -> Result<Self, Self::Err> {
195        if !s.is_empty() {
196            Ok(Language(s.to_ascii_lowercase()))
197        } else {
198            Err("invalid language")
199        }
200    }
201}
202
203// In harfbuzz, despite having `hb_script_t`, script can actually have any tag.
204// So we're doing the same.
205// The only difference is that `Script` cannot be set to `HB_SCRIPT_INVALID`.
206/// A text script.
207#[allow(missing_docs)]
208#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
209pub struct Script(pub(crate) Tag);
210
211impl Script {
212    #[inline]
213    pub(crate) const fn from_bytes(bytes: &[u8; 4]) -> Self {
214        Script(Tag::from_bytes(bytes))
215    }
216
217    /// Converts an ISO 15924 script tag to a corresponding `Script`.
218    pub fn from_iso15924_tag(tag: Tag) -> Option<Script> {
219        if tag.is_null() {
220            return None;
221        }
222
223        // Be lenient, adjust case (one capital letter followed by three small letters).
224        let tag = Tag((tag.as_u32() & 0xDFDFDFDF) | 0x00202020);
225
226        match &tag.to_bytes() {
227            // These graduated from the 'Q' private-area codes, but
228            // the old code is still aliased by Unicode, and the Qaai
229            // one in use by ICU.
230            b"Qaai" => return Some(script::INHERITED),
231            b"Qaac" => return Some(script::COPTIC),
232
233            // Script variants from https://unicode.org/iso15924/
234            b"Aran" => return Some(script::ARABIC),
235            b"Cyrs" => return Some(script::CYRILLIC),
236            b"Geok" => return Some(script::GEORGIAN),
237            b"Hans" | b"Hant" => return Some(script::HAN),
238            b"Jamo" => return Some(script::HANGUL),
239            b"Latf" | b"Latg" => return Some(script::LATIN),
240            b"Syre" | b"Syrj" | b"Syrn" => return Some(script::SYRIAC),
241
242            _ => {}
243        }
244
245        if tag.as_u32() & 0xE0E0E0E0 == 0x40606060 {
246            Some(Script(tag))
247        } else {
248            Some(script::UNKNOWN)
249        }
250    }
251
252    /// Returns script's tag.
253    #[inline]
254    pub fn tag(&self) -> Tag {
255        self.0
256    }
257}
258
259impl core::str::FromStr for Script {
260    type Err = &'static str;
261
262    fn from_str(s: &str) -> Result<Self, Self::Err> {
263        let tag = Tag::from_bytes_lossy(s.as_bytes());
264        Script::from_iso15924_tag(tag).ok_or("invalid script")
265    }
266}
267
268/// Predefined scripts.
269pub mod script {
270    #![allow(missing_docs)]
271
272    use crate::Script;
273
274    // Since 1.1
275    pub const COMMON: Script = Script::from_bytes(b"Zyyy");
276    pub const INHERITED: Script = Script::from_bytes(b"Zinh");
277    pub const ARABIC: Script = Script::from_bytes(b"Arab");
278    pub const ARMENIAN: Script = Script::from_bytes(b"Armn");
279    pub const BENGALI: Script = Script::from_bytes(b"Beng");
280    pub const CYRILLIC: Script = Script::from_bytes(b"Cyrl");
281    pub const DEVANAGARI: Script = Script::from_bytes(b"Deva");
282    pub const GEORGIAN: Script = Script::from_bytes(b"Geor");
283    pub const GREEK: Script = Script::from_bytes(b"Grek");
284    pub const GUJARATI: Script = Script::from_bytes(b"Gujr");
285    pub const GURMUKHI: Script = Script::from_bytes(b"Guru");
286    pub const HANGUL: Script = Script::from_bytes(b"Hang");
287    pub const HAN: Script = Script::from_bytes(b"Hani");
288    pub const HEBREW: Script = Script::from_bytes(b"Hebr");
289    pub const HIRAGANA: Script = Script::from_bytes(b"Hira");
290    pub const KANNADA: Script = Script::from_bytes(b"Knda");
291    pub const KATAKANA: Script = Script::from_bytes(b"Kana");
292    pub const LAO: Script = Script::from_bytes(b"Laoo");
293    pub const LATIN: Script = Script::from_bytes(b"Latn");
294    pub const MALAYALAM: Script = Script::from_bytes(b"Mlym");
295    pub const ORIYA: Script = Script::from_bytes(b"Orya");
296    pub const TAMIL: Script = Script::from_bytes(b"Taml");
297    pub const TELUGU: Script = Script::from_bytes(b"Telu");
298    pub const THAI: Script = Script::from_bytes(b"Thai");
299    // Since 2.0
300    pub const TIBETAN: Script = Script::from_bytes(b"Tibt");
301    // Since 3.0
302    pub const BOPOMOFO: Script = Script::from_bytes(b"Bopo");
303    pub const BRAILLE: Script = Script::from_bytes(b"Brai");
304    pub const CANADIAN_SYLLABICS: Script = Script::from_bytes(b"Cans");
305    pub const CHEROKEE: Script = Script::from_bytes(b"Cher");
306    pub const ETHIOPIC: Script = Script::from_bytes(b"Ethi");
307    pub const KHMER: Script = Script::from_bytes(b"Khmr");
308    pub const MONGOLIAN: Script = Script::from_bytes(b"Mong");
309    pub const MYANMAR: Script = Script::from_bytes(b"Mymr");
310    pub const OGHAM: Script = Script::from_bytes(b"Ogam");
311    pub const RUNIC: Script = Script::from_bytes(b"Runr");
312    pub const SINHALA: Script = Script::from_bytes(b"Sinh");
313    pub const SYRIAC: Script = Script::from_bytes(b"Syrc");
314    pub const THAANA: Script = Script::from_bytes(b"Thaa");
315    pub const YI: Script = Script::from_bytes(b"Yiii");
316    // Since 3.1
317    pub const DESERET: Script = Script::from_bytes(b"Dsrt");
318    pub const GOTHIC: Script = Script::from_bytes(b"Goth");
319    pub const OLD_ITALIC: Script = Script::from_bytes(b"Ital");
320    // Since 3.2
321    pub const BUHID: Script = Script::from_bytes(b"Buhd");
322    pub const HANUNOO: Script = Script::from_bytes(b"Hano");
323    pub const TAGALOG: Script = Script::from_bytes(b"Tglg");
324    pub const TAGBANWA: Script = Script::from_bytes(b"Tagb");
325    // Since 4.0
326    pub const CYPRIOT: Script = Script::from_bytes(b"Cprt");
327    pub const LIMBU: Script = Script::from_bytes(b"Limb");
328    pub const LINEAR_B: Script = Script::from_bytes(b"Linb");
329    pub const OSMANYA: Script = Script::from_bytes(b"Osma");
330    pub const SHAVIAN: Script = Script::from_bytes(b"Shaw");
331    pub const TAI_LE: Script = Script::from_bytes(b"Tale");
332    pub const UGARITIC: Script = Script::from_bytes(b"Ugar");
333    // Since 4.1
334    pub const BUGINESE: Script = Script::from_bytes(b"Bugi");
335    pub const COPTIC: Script = Script::from_bytes(b"Copt");
336    pub const GLAGOLITIC: Script = Script::from_bytes(b"Glag");
337    pub const KHAROSHTHI: Script = Script::from_bytes(b"Khar");
338    pub const NEW_TAI_LUE: Script = Script::from_bytes(b"Talu");
339    pub const OLD_PERSIAN: Script = Script::from_bytes(b"Xpeo");
340    pub const SYLOTI_NAGRI: Script = Script::from_bytes(b"Sylo");
341    pub const TIFINAGH: Script = Script::from_bytes(b"Tfng");
342    // Since 5.0
343    pub const UNKNOWN: Script = Script::from_bytes(b"Zzzz"); // Script can be Unknown, but not Invalid.
344    pub const BALINESE: Script = Script::from_bytes(b"Bali");
345    pub const CUNEIFORM: Script = Script::from_bytes(b"Xsux");
346    pub const NKO: Script = Script::from_bytes(b"Nkoo");
347    pub const PHAGS_PA: Script = Script::from_bytes(b"Phag");
348    pub const PHOENICIAN: Script = Script::from_bytes(b"Phnx");
349    // Since 5.1
350    pub const CARIAN: Script = Script::from_bytes(b"Cari");
351    pub const CHAM: Script = Script::from_bytes(b"Cham");
352    pub const KAYAH_LI: Script = Script::from_bytes(b"Kali");
353    pub const LEPCHA: Script = Script::from_bytes(b"Lepc");
354    pub const LYCIAN: Script = Script::from_bytes(b"Lyci");
355    pub const LYDIAN: Script = Script::from_bytes(b"Lydi");
356    pub const OL_CHIKI: Script = Script::from_bytes(b"Olck");
357    pub const REJANG: Script = Script::from_bytes(b"Rjng");
358    pub const SAURASHTRA: Script = Script::from_bytes(b"Saur");
359    pub const SUNDANESE: Script = Script::from_bytes(b"Sund");
360    pub const VAI: Script = Script::from_bytes(b"Vaii");
361    // Since 5.2
362    pub const AVESTAN: Script = Script::from_bytes(b"Avst");
363    pub const BAMUM: Script = Script::from_bytes(b"Bamu");
364    pub const EGYPTIAN_HIEROGLYPHS: Script = Script::from_bytes(b"Egyp");
365    pub const IMPERIAL_ARAMAIC: Script = Script::from_bytes(b"Armi");
366    pub const INSCRIPTIONAL_PAHLAVI: Script = Script::from_bytes(b"Phli");
367    pub const INSCRIPTIONAL_PARTHIAN: Script = Script::from_bytes(b"Prti");
368    pub const JAVANESE: Script = Script::from_bytes(b"Java");
369    pub const KAITHI: Script = Script::from_bytes(b"Kthi");
370    pub const LISU: Script = Script::from_bytes(b"Lisu");
371    pub const MEETEI_MAYEK: Script = Script::from_bytes(b"Mtei");
372    pub const OLD_SOUTH_ARABIAN: Script = Script::from_bytes(b"Sarb");
373    pub const OLD_TURKIC: Script = Script::from_bytes(b"Orkh");
374    pub const SAMARITAN: Script = Script::from_bytes(b"Samr");
375    pub const TAI_THAM: Script = Script::from_bytes(b"Lana");
376    pub const TAI_VIET: Script = Script::from_bytes(b"Tavt");
377    // Since 6.0
378    pub const BATAK: Script = Script::from_bytes(b"Batk");
379    pub const BRAHMI: Script = Script::from_bytes(b"Brah");
380    pub const MANDAIC: Script = Script::from_bytes(b"Mand");
381    // Since 6.1
382    pub const CHAKMA: Script = Script::from_bytes(b"Cakm");
383    pub const MEROITIC_CURSIVE: Script = Script::from_bytes(b"Merc");
384    pub const MEROITIC_HIEROGLYPHS: Script = Script::from_bytes(b"Mero");
385    pub const MIAO: Script = Script::from_bytes(b"Plrd");
386    pub const SHARADA: Script = Script::from_bytes(b"Shrd");
387    pub const SORA_SOMPENG: Script = Script::from_bytes(b"Sora");
388    pub const TAKRI: Script = Script::from_bytes(b"Takr");
389    // Since 7.0
390    pub const BASSA_VAH: Script = Script::from_bytes(b"Bass");
391    pub const CAUCASIAN_ALBANIAN: Script = Script::from_bytes(b"Aghb");
392    pub const DUPLOYAN: Script = Script::from_bytes(b"Dupl");
393    pub const ELBASAN: Script = Script::from_bytes(b"Elba");
394    pub const GRANTHA: Script = Script::from_bytes(b"Gran");
395    pub const KHOJKI: Script = Script::from_bytes(b"Khoj");
396    pub const KHUDAWADI: Script = Script::from_bytes(b"Sind");
397    pub const LINEAR_A: Script = Script::from_bytes(b"Lina");
398    pub const MAHAJANI: Script = Script::from_bytes(b"Mahj");
399    pub const MANICHAEAN: Script = Script::from_bytes(b"Mani");
400    pub const MENDE_KIKAKUI: Script = Script::from_bytes(b"Mend");
401    pub const MODI: Script = Script::from_bytes(b"Modi");
402    pub const MRO: Script = Script::from_bytes(b"Mroo");
403    pub const NABATAEAN: Script = Script::from_bytes(b"Nbat");
404    pub const OLD_NORTH_ARABIAN: Script = Script::from_bytes(b"Narb");
405    pub const OLD_PERMIC: Script = Script::from_bytes(b"Perm");
406    pub const PAHAWH_HMONG: Script = Script::from_bytes(b"Hmng");
407    pub const PALMYRENE: Script = Script::from_bytes(b"Palm");
408    pub const PAU_CIN_HAU: Script = Script::from_bytes(b"Pauc");
409    pub const PSALTER_PAHLAVI: Script = Script::from_bytes(b"Phlp");
410    pub const SIDDHAM: Script = Script::from_bytes(b"Sidd");
411    pub const TIRHUTA: Script = Script::from_bytes(b"Tirh");
412    pub const WARANG_CITI: Script = Script::from_bytes(b"Wara");
413    // Since 8.0
414    pub const AHOM: Script = Script::from_bytes(b"Ahom");
415    pub const ANATOLIAN_HIEROGLYPHS: Script = Script::from_bytes(b"Hluw");
416    pub const HATRAN: Script = Script::from_bytes(b"Hatr");
417    pub const MULTANI: Script = Script::from_bytes(b"Mult");
418    pub const OLD_HUNGARIAN: Script = Script::from_bytes(b"Hung");
419    pub const SIGNWRITING: Script = Script::from_bytes(b"Sgnw");
420    // Since 9.0
421    pub const ADLAM: Script = Script::from_bytes(b"Adlm");
422    pub const BHAIKSUKI: Script = Script::from_bytes(b"Bhks");
423    pub const MARCHEN: Script = Script::from_bytes(b"Marc");
424    pub const OSAGE: Script = Script::from_bytes(b"Osge");
425    pub const TANGUT: Script = Script::from_bytes(b"Tang");
426    pub const NEWA: Script = Script::from_bytes(b"Newa");
427    // Since 10.0
428    pub const MASARAM_GONDI: Script = Script::from_bytes(b"Gonm");
429    pub const NUSHU: Script = Script::from_bytes(b"Nshu");
430    pub const SOYOMBO: Script = Script::from_bytes(b"Soyo");
431    pub const ZANABAZAR_SQUARE: Script = Script::from_bytes(b"Zanb");
432    // Since 11.0
433    pub const DOGRA: Script = Script::from_bytes(b"Dogr");
434    pub const GUNJALA_GONDI: Script = Script::from_bytes(b"Gong");
435    pub const HANIFI_ROHINGYA: Script = Script::from_bytes(b"Rohg");
436    pub const MAKASAR: Script = Script::from_bytes(b"Maka");
437    pub const MEDEFAIDRIN: Script = Script::from_bytes(b"Medf");
438    pub const OLD_SOGDIAN: Script = Script::from_bytes(b"Sogo");
439    pub const SOGDIAN: Script = Script::from_bytes(b"Sogd");
440    // Since 12.0
441    pub const ELYMAIC: Script = Script::from_bytes(b"Elym");
442    pub const NANDINAGARI: Script = Script::from_bytes(b"Nand");
443    pub const NYIAKENG_PUACHUE_HMONG: Script = Script::from_bytes(b"Hmnp");
444    pub const WANCHO: Script = Script::from_bytes(b"Wcho");
445    // Since 13.0
446    pub const CHORASMIAN: Script = Script::from_bytes(b"Chrs");
447    pub const DIVES_AKURU: Script = Script::from_bytes(b"Diak");
448    pub const KHITAN_SMALL_SCRIPT: Script = Script::from_bytes(b"Kits");
449    pub const YEZIDI: Script = Script::from_bytes(b"Yezi");
450    // Since 14.0
451    pub const CYPRO_MINOAN: Script = Script::from_bytes(b"Cpmn");
452    pub const OLD_UYGHUR: Script = Script::from_bytes(b"Ougr");
453    pub const TANGSA: Script = Script::from_bytes(b"Tnsa");
454    pub const TOTO: Script = Script::from_bytes(b"Toto");
455    pub const VITHKUQI: Script = Script::from_bytes(b"Vith");
456
457    pub const SCRIPT_MATH: Script = Script::from_bytes(b"Zmth");
458
459    // https://github.com/harfbuzz/harfbuzz/issues/1162
460    pub const MYANMAR_ZAWGYI: Script = Script::from_bytes(b"Qaag");
461}
462
463/// A feature tag with an accompanying range specifying on which subslice of
464/// `shape`s input it should be applied.
465#[repr(C)]
466#[allow(missing_docs)]
467#[derive(Clone, Copy, PartialEq, Hash, Debug)]
468pub struct Feature {
469    pub tag: Tag,
470    pub value: u32,
471    pub start: u32,
472    pub end: u32,
473}
474
475impl Feature {
476    /// Create a new `Feature` struct.
477    pub fn new(tag: Tag, value: u32, range: impl RangeBounds<usize>) -> Feature {
478        let max = u32::MAX as usize;
479        let start = match range.start_bound() {
480            Bound::Included(&included) => included.min(max) as u32,
481            Bound::Excluded(&excluded) => excluded.min(max - 1) as u32 + 1,
482            Bound::Unbounded => 0,
483        };
484        let end = match range.end_bound() {
485            Bound::Included(&included) => included.min(max) as u32,
486            Bound::Excluded(&excluded) => excluded.saturating_sub(1).min(max) as u32,
487            Bound::Unbounded => max as u32,
488        };
489
490        Feature {
491            tag,
492            value,
493            start,
494            end,
495        }
496    }
497
498    pub(crate) fn is_global(&self) -> bool {
499        self.start == 0 && self.end == u32::MAX
500    }
501}
502
503impl core::str::FromStr for Feature {
504    type Err = &'static str;
505
506    /// Parses a `Feature` form a string.
507    ///
508    /// Possible values:
509    ///
510    /// - `kern` -> kern .. 1
511    /// - `+kern` -> kern .. 1
512    /// - `-kern` -> kern .. 0
513    /// - `kern=0` -> kern .. 0
514    /// - `kern=1` -> kern .. 1
515    /// - `aalt=2` -> altr .. 2
516    /// - `kern[]` -> kern .. 1
517    /// - `kern[:]` -> kern .. 1
518    /// - `kern[5:]` -> kern 5.. 1
519    /// - `kern[:5]` -> kern ..=5 1
520    /// - `kern[3:5]` -> kern 3..=5 1
521    /// - `kern[3]` -> kern 3..=4 1
522    /// - `aalt[3:5]=2` -> kern 3..=5 1
523    fn from_str(s: &str) -> Result<Self, Self::Err> {
524        fn parse(s: &str) -> Option<Feature> {
525            if s.is_empty() {
526                return None;
527            }
528
529            let mut p = TextParser::new(s);
530
531            // Parse prefix.
532            let mut value = 1;
533            match p.curr_byte()? {
534                b'-' => {
535                    value = 0;
536                    p.advance(1);
537                }
538                b'+' => {
539                    value = 1;
540                    p.advance(1);
541                }
542                _ => {}
543            }
544
545            // Parse tag.
546            p.skip_spaces();
547            let quote = p.consume_quote();
548
549            let tag = p.consume_tag()?;
550
551            // Force closing quote.
552            if let Some(quote) = quote {
553                p.consume_byte(quote)?;
554            }
555
556            // Parse indices.
557            p.skip_spaces();
558
559            let (start, end) = if p.consume_byte(b'[').is_some() {
560                let start_opt = p.consume_i32();
561                let start = start_opt.unwrap_or(0) as u32; // negative value overflow is ok
562
563                let end = if matches!(p.curr_byte(), Some(b':') | Some(b';')) {
564                    p.advance(1);
565                    p.consume_i32().unwrap_or(-1) as u32 // negative value overflow is ok
566                } else {
567                    if start_opt.is_some() && start != core::u32::MAX {
568                        start + 1
569                    } else {
570                        core::u32::MAX
571                    }
572                };
573
574                p.consume_byte(b']')?;
575
576                (start, end)
577            } else {
578                (0, core::u32::MAX)
579            };
580
581            // Parse postfix.
582            let had_equal = p.consume_byte(b'=').is_some();
583            let value1 = p
584                .consume_i32()
585                .or_else(|| p.consume_bool().map(|b| b as i32));
586
587            if had_equal && value1.is_none() {
588                return None;
589            };
590
591            if let Some(value1) = value1 {
592                value = value1 as u32; // negative value overflow is ok
593            }
594
595            p.skip_spaces();
596
597            if !p.at_end() {
598                return None;
599            }
600
601            Some(Feature {
602                tag,
603                value,
604                start,
605                end,
606            })
607        }
608
609        parse(s).ok_or("invalid feature")
610    }
611}
612
613#[cfg(test)]
614mod tests_features {
615    use super::*;
616    use core::str::FromStr;
617
618    macro_rules! test {
619        ($name:ident, $text:expr, $tag:expr, $value:expr, $range:expr) => {
620            #[test]
621            fn $name() {
622                assert_eq!(
623                    Feature::from_str($text).unwrap(),
624                    Feature::new(Tag::from_bytes($tag), $value, $range)
625                );
626            }
627        };
628    }
629
630    test!(parse_01, "kern", b"kern", 1, ..);
631    test!(parse_02, "+kern", b"kern", 1, ..);
632    test!(parse_03, "-kern", b"kern", 0, ..);
633    test!(parse_04, "kern=0", b"kern", 0, ..);
634    test!(parse_05, "kern=1", b"kern", 1, ..);
635    test!(parse_06, "kern=2", b"kern", 2, ..);
636    test!(parse_07, "kern[]", b"kern", 1, ..);
637    test!(parse_08, "kern[:]", b"kern", 1, ..);
638    test!(parse_09, "kern[5:]", b"kern", 1, 5..);
639    test!(parse_10, "kern[:5]", b"kern", 1, ..=5);
640    test!(parse_11, "kern[3:5]", b"kern", 1, 3..=5);
641    test!(parse_12, "kern[3]", b"kern", 1, 3..=4);
642    test!(parse_13, "kern[3:5]=2", b"kern", 2, 3..=5);
643    test!(parse_14, "kern[3;5]=2", b"kern", 2, 3..=5);
644    test!(parse_15, "kern[:-1]", b"kern", 1, ..);
645    test!(parse_16, "kern[-1]", b"kern", 1, core::u32::MAX as usize..);
646    test!(parse_17, "kern=on", b"kern", 1, ..);
647    test!(parse_18, "kern=off", b"kern", 0, ..);
648    test!(parse_19, "kern=oN", b"kern", 1, ..);
649    test!(parse_20, "kern=oFf", b"kern", 0, ..);
650}
651
652/// A font variation.
653#[repr(C)]
654#[allow(missing_docs)]
655#[derive(Clone, Copy, PartialEq, Debug)]
656pub struct Variation {
657    pub tag: Tag,
658    pub value: f32,
659}
660
661impl core::str::FromStr for Variation {
662    type Err = &'static str;
663
664    fn from_str(s: &str) -> Result<Self, Self::Err> {
665        fn parse(s: &str) -> Option<Variation> {
666            if s.is_empty() {
667                return None;
668            }
669
670            let mut p = TextParser::new(s);
671
672            // Parse tag.
673            p.skip_spaces();
674            let quote = p.consume_quote();
675
676            let tag = p.consume_tag()?;
677
678            // Force closing quote.
679            if let Some(quote) = quote {
680                p.consume_byte(quote)?;
681            }
682
683            let _ = p.consume_byte(b'=');
684            let value = p.consume_f32()?;
685            p.skip_spaces();
686
687            if !p.at_end() {
688                return None;
689            }
690
691            Some(Variation { tag, value })
692        }
693
694        parse(s).ok_or("invalid variation")
695    }
696}
697
698pub trait TagExt {
699    fn default_script() -> Self;
700    fn default_language() -> Self;
701    fn to_lowercase(&self) -> Self;
702    fn to_uppercase(&self) -> Self;
703}
704
705impl TagExt for Tag {
706    #[inline]
707    fn default_script() -> Self {
708        Tag::from_bytes(b"DFLT")
709    }
710
711    #[inline]
712    fn default_language() -> Self {
713        Tag::from_bytes(b"dflt")
714    }
715
716    /// Converts tag to lowercase.
717    #[inline]
718    fn to_lowercase(&self) -> Self {
719        let b = self.to_bytes();
720        Tag::from_bytes(&[
721            b[0].to_ascii_lowercase(),
722            b[1].to_ascii_lowercase(),
723            b[2].to_ascii_lowercase(),
724            b[3].to_ascii_lowercase(),
725        ])
726    }
727
728    /// Converts tag to uppercase.
729    #[inline]
730    fn to_uppercase(&self) -> Self {
731        let b = self.to_bytes();
732        Tag::from_bytes(&[
733            b[0].to_ascii_uppercase(),
734            b[1].to_ascii_uppercase(),
735            b[2].to_ascii_uppercase(),
736            b[3].to_ascii_uppercase(),
737        ])
738    }
739}