Skip to main content

harfrust/hb/
common.rs

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