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