Skip to main content

harfrust/hb/
tag.rs

1use smallvec::SmallVec;
2
3use super::common::TagExt;
4use super::{hb_tag_t, script, tag_table, Language, Script};
5
6type ThreeTags = SmallVec<[hb_tag_t; 3]>;
7
8trait SmallVecExt {
9    fn left(&self) -> usize;
10    fn is_full(&self) -> bool;
11}
12
13impl<A: smallvec::Array> SmallVecExt for SmallVec<A> {
14    fn left(&self) -> usize {
15        self.inline_size() - self.len()
16    }
17
18    fn is_full(&self) -> bool {
19        self.len() == self.inline_size()
20    }
21}
22
23/// Converts an `Script` and an `Language` to script and language tags.
24pub fn tags_from_script_and_language(
25    script: Option<Script>,
26    language: Option<&Language>,
27) -> (ThreeTags, ThreeTags) {
28    let mut needs_script = true;
29    let mut scripts = SmallVec::new();
30    let mut languages = SmallVec::new();
31
32    let mut private_use_subtag = None;
33    let mut prefix = b"" as &[u8];
34    if let Some(language) = language {
35        let language = language.as_bytes();
36        if language.starts_with(b"x-") {
37            private_use_subtag = Some(language);
38        } else {
39            let mut i = 1;
40            while i < language.len() {
41                if language.get(i - 1) == Some(&b'-') && language.get(i + 1) == Some(&b'-') {
42                    if language[i] == b'x' {
43                        private_use_subtag = Some(&language[i..]);
44                        if prefix.is_empty() {
45                            prefix = &language[..i - 1];
46                        }
47
48                        break;
49                    } else {
50                        prefix = &language[..i - 1];
51                    }
52                }
53
54                i += 1;
55            }
56
57            if prefix.is_empty() {
58                prefix = &language[..i];
59            }
60        }
61
62        needs_script = !parse_private_use_subtag(
63            private_use_subtag,
64            b"-hbsc",
65            u8::to_ascii_lowercase,
66            &mut scripts,
67        );
68
69        let needs_language = !parse_private_use_subtag(
70            private_use_subtag,
71            b"-hbot",
72            u8::to_ascii_uppercase,
73            &mut languages,
74        );
75
76        if needs_language {
77            if let Some(language) = Language::new(prefix) {
78                tags_from_language(&language, &mut languages);
79            }
80        }
81    }
82
83    if needs_script {
84        all_tags_from_script(script, &mut scripts);
85    }
86
87    (scripts, languages)
88}
89
90fn parse_private_use_subtag(
91    private_use_subtag: Option<&[u8]>,
92    prefix: &[u8],
93    normalize: fn(&u8) -> u8,
94    tags: &mut ThreeTags,
95) -> bool {
96    let Some(private_use_subtag) = private_use_subtag else {
97        return false;
98    };
99
100    let private_use_subtag = match private_use_subtag
101        .windows(prefix.len())
102        .position(|window| window == prefix)
103    {
104        Some(idx) => &private_use_subtag[idx + prefix.len()..],
105        None => return false,
106    };
107
108    let mut tag = SmallVec::<[u8; 4]>::new();
109    for c in private_use_subtag.iter().take(4) {
110        if c.is_ascii_alphanumeric() {
111            tag.push((normalize)(c));
112        } else {
113            break;
114        }
115    }
116
117    if tag.is_empty() {
118        return false;
119    }
120
121    let mut tag = hb_tag_t::from_bytes_lossy(tag.as_slice());
122
123    // Some bits magic from HarfBuzz...
124    if tag.as_u32() & 0xDFDF_DFDF == hb_tag_t::default_script().as_u32() {
125        tag = hb_tag_t::from_u32(tag.as_u32() ^ !0xDFDF_DFDF);
126    }
127
128    tags.push(tag);
129
130    true
131}
132
133fn lang_cmp(s1: &[u8], s2: &[u8]) -> core::cmp::Ordering {
134    let da = s1
135        .iter()
136        .position(|&c| c == b'-' || c == b'\0')
137        .unwrap_or(s1.len());
138    let db = s2
139        .iter()
140        .position(|&c| c == b'-' || c == b'\0')
141        .unwrap_or(s2.len());
142    let n = core::cmp::max(da, db);
143    let ea = core::cmp::min(n, s1.len());
144    let eb = core::cmp::min(n, s2.len());
145    s1[..ea].cmp(&s2[..eb])
146}
147
148pub(super) fn subtag_matches(language: impl AsRef<[u8]>, subtag: impl AsRef<[u8]>) -> bool {
149    let language = language.as_ref();
150    let subtag = subtag.as_ref();
151
152    for i in language
153        .windows(subtag.len())
154        .enumerate()
155        .filter_map(|(i, window)| (window == subtag).then_some(i))
156    {
157        if let Some(c) = language.get(i + subtag.len()) {
158            if !c.is_ascii_alphanumeric() {
159                return true;
160            }
161        } else {
162            return true;
163        }
164    }
165
166    false
167}
168
169pub(super) fn lang_matches(language: impl AsRef<[u8]>, spec: impl AsRef<[u8]>) -> bool {
170    let language = language.as_ref();
171    let spec = spec.as_ref();
172
173    if language.starts_with(spec) {
174        return language.len() == spec.len() || language.get(spec.len()) == Some(&b'-');
175    }
176
177    false
178}
179
180pub(super) fn strncmp(s1: impl AsRef<[u8]>, s2: impl AsRef<[u8]>, n: usize) -> bool {
181    let s1 = s1.as_ref();
182    let s2 = s2.as_ref();
183
184    let n1 = core::cmp::min(n, s1.len());
185    let n2 = core::cmp::min(n, s2.len());
186    s1[..n1] == s2[..n2]
187}
188
189fn tags_from_language(language: &Language, tags: &mut ThreeTags) {
190    let language = language.as_bytes();
191
192    // Check for matches of multiple subtags.
193    if let Ok(language_str) = core::str::from_utf8(language) {
194        if tag_table::tags_from_complex_language(language_str, tags) {
195            return;
196        }
197    }
198
199    let mut sublang = language;
200
201    // Find a language matching in the first component.
202    if let Some(i) = language.iter().position(|&c| c == b'-') {
203        // If there is an extended language tag, use it.
204        if language.len() >= 6 {
205            let extlang = match language[i + 1..].iter().position(|&c| c == b'-') {
206                Some(idx) => idx == 3,
207                None => language.len() - i - 1 == 3,
208            };
209
210            if extlang && language[i + 1].is_ascii_alphabetic() {
211                sublang = &language[i + 1..];
212            }
213        }
214    }
215
216    use tag_table::OPEN_TYPE_LANGUAGES as LANGUAGES;
217
218    if let Ok(mut idx) = LANGUAGES.binary_search_by(|v| lang_cmp(&v.language, sublang)) {
219        while idx != 0 && LANGUAGES[idx].language == LANGUAGES[idx - 1].language {
220            idx -= 1;
221        }
222
223        let len = core::cmp::min(tags.left(), LANGUAGES.len() - idx - 1);
224        for i in 0..len {
225            if LANGUAGES[idx + i].language != LANGUAGES[idx].language {
226                break;
227            }
228
229            if LANGUAGES[idx + i].tag.is_null() {
230                break;
231            }
232
233            if tags.is_full() {
234                break;
235            }
236
237            tags.push(LANGUAGES[idx + i].tag);
238        }
239
240        return;
241    }
242
243    if language.len() == 3 {
244        tags.push(hb_tag_t::from_bytes_lossy(language).to_uppercase());
245    }
246}
247
248fn all_tags_from_script(script: Option<Script>, tags: &mut ThreeTags) {
249    if let Some(script) = script {
250        if let Some(tag) = new_tag_from_script(script) {
251            // Script::Myanmar maps to 'mym2', but there is no 'mym3'.
252            if tag != hb_tag_t::new(b"mym2") {
253                let mut tag3 = tag.to_be_bytes();
254                tag3[3] = b'3';
255                tags.push(hb_tag_t::new(&tag3));
256            }
257
258            if !tags.is_full() {
259                tags.push(tag);
260            }
261        }
262
263        if !tags.is_full() {
264            tags.push(old_tag_from_script(script));
265        }
266    }
267}
268
269fn new_tag_from_script(script: Script) -> Option<hb_tag_t> {
270    match script {
271        script::BENGALI => Some(hb_tag_t::new(b"bng2")),
272        script::DEVANAGARI => Some(hb_tag_t::new(b"dev2")),
273        script::GUJARATI => Some(hb_tag_t::new(b"gjr2")),
274        script::GURMUKHI => Some(hb_tag_t::new(b"gur2")),
275        script::KANNADA => Some(hb_tag_t::new(b"knd2")),
276        script::MALAYALAM => Some(hb_tag_t::new(b"mlm2")),
277        script::ORIYA => Some(hb_tag_t::new(b"ory2")),
278        script::TAMIL => Some(hb_tag_t::new(b"tml2")),
279        script::TELUGU => Some(hb_tag_t::new(b"tel2")),
280        script::MYANMAR => Some(hb_tag_t::new(b"mym2")),
281        _ => None,
282    }
283}
284
285fn old_tag_from_script(script: Script) -> hb_tag_t {
286    // This seems to be accurate as of end of 2012.
287    match script {
288        script::MATH => hb_tag_t::new(b"math"),
289
290        // Katakana and Hiragana both map to 'kana'.
291        script::HIRAGANA => hb_tag_t::new(b"kana"),
292
293        // Spaces at the end are preserved, unlike ISO 15924.
294        script::LAO => hb_tag_t::new(b"lao "),
295        script::YI => hb_tag_t::new(b"yi  "),
296        // Unicode-5.0 additions.
297        script::NKO => hb_tag_t::new(b"nko "),
298        // Unicode-5.1 additions.
299        script::VAI => hb_tag_t::new(b"vai "),
300
301        // Else, just change first char to lowercase and return.
302        _ => hb_tag_t::from_u32(script.tag().as_u32() | 0x2000_0000),
303    }
304}
305
306#[rustfmt::skip]
307#[cfg(test)]
308mod tests {
309    #![allow(non_snake_case)]
310
311    use super::*;
312    use alloc::vec::Vec;
313
314    fn new_tag_to_script(tag: hb_tag_t) -> Option<Script> {
315        match &tag.to_be_bytes() {
316            b"bng2" => Some(script::BENGALI),
317            b"dev2" => Some(script::DEVANAGARI),
318            b"gjr2" => Some(script::GUJARATI),
319            b"gur2" => Some(script::GURMUKHI),
320            b"knd2" => Some(script::KANNADA),
321            b"mlm2" => Some(script::MALAYALAM),
322            b"ory2" => Some(script::ORIYA),
323            b"tml2" => Some(script::TAMIL),
324            b"tel2" => Some(script::TELUGU),
325            b"mym2" => Some(script::MYANMAR),
326            _ => Some(script::UNKNOWN),
327        }
328    }
329
330    fn old_tag_to_script(tag: hb_tag_t) -> Option<Script> {
331        if tag == hb_tag_t::default_script() {
332            return None;
333        }
334
335        let mut bytes = tag.to_be_bytes();
336
337        // This side of the conversion is fully algorithmic.
338
339        // Any spaces at the end of the tag are replaced by repeating the last
340        // letter.  Eg 'nko ' -> 'Nkoo'
341        if bytes[2] == b' ' {
342            bytes[2] = bytes[1];
343        }
344        if bytes[3] == b' ' {
345            bytes[3] = bytes[2];
346        }
347
348        // Change first char to uppercase.
349        bytes[0] = bytes[0].to_ascii_uppercase();
350
351        Some(Script(hb_tag_t::new(&bytes)))
352    }
353
354    fn tag_to_script(tag: hb_tag_t) -> Option<Script> {
355        let bytes = tag.to_be_bytes();
356        if bytes[3] == b'2' || bytes[3] == b'3' {
357            let mut tag2 = bytes;
358            tag2[3] = b'2';
359            return new_tag_to_script(hb_tag_t::new(&tag2));
360        }
361
362        old_tag_to_script(tag)
363    }
364
365    fn test_simple_tags(tag: &str, script: Script) {
366        let tag = hb_tag_t::from_bytes_lossy(tag.as_bytes());
367
368        let (scripts, _) = tags_from_script_and_language(Some(script), None);
369        if !scripts.is_empty() {
370            assert_eq!(tag, scripts[0]);
371        } else {
372            assert_eq!(tag, hb_tag_t::default_script());
373        }
374
375        assert_eq!(tag_to_script(tag), Some(script));
376    }
377
378    #[test]
379    fn tag_to_uppercase() {
380        assert_eq!(hb_tag_t::new(b"abcd").to_uppercase(), hb_tag_t::new(b"ABCD"));
381        assert_eq!(hb_tag_t::new(b"abc ").to_uppercase(), hb_tag_t::new(b"ABC "));
382        assert_eq!(hb_tag_t::new(b"ABCD").to_uppercase(), hb_tag_t::new(b"ABCD"));
383    }
384
385    #[test]
386    fn tag_to_lowercase() {
387        assert_eq!(hb_tag_t::new(b"abcd").to_lowercase(), hb_tag_t::new(b"abcd"));
388        assert_eq!(hb_tag_t::new(b"abc ").to_lowercase(), hb_tag_t::new(b"abc "));
389        assert_eq!(hb_tag_t::new(b"ABCD").to_lowercase(), hb_tag_t::new(b"abcd"));
390    }
391
392    #[test]
393    fn script_degenerate() {
394        assert_eq!(hb_tag_t::new(b"DFLT"), hb_tag_t::default_script());
395
396        // Hiragana and Katakana both map to 'kana'.
397        test_simple_tags("kana", script::KATAKANA);
398
399        let (scripts, _) = tags_from_script_and_language(Some(script::HIRAGANA), None);
400        assert_eq!(scripts.as_slice(), &[hb_tag_t::new(b"kana")]);
401
402        // Spaces are replaced
403        assert_eq!(tag_to_script(hb_tag_t::new(b"be  ")), Script::from_iso15924_tag(hb_tag_t::new(b"Beee")));
404    }
405
406    #[test]
407    fn script_simple() {
408        // Arbitrary non-existent script.
409        test_simple_tags("wwyz", Script::from_iso15924_tag(hb_tag_t::new(b"wWyZ")).unwrap());
410
411        // These we don't really care about.
412        test_simple_tags("zyyy", script::COMMON);
413        test_simple_tags("zinh", script::INHERITED);
414        test_simple_tags("zzzz", script::UNKNOWN);
415
416        test_simple_tags("arab", script::ARABIC);
417        test_simple_tags("copt", script::COPTIC);
418        test_simple_tags("kana", script::KATAKANA);
419        test_simple_tags("latn", script::LATIN);
420
421        // These are trickier since their OT script tags have space.
422        test_simple_tags("lao ", script::LAO);
423        test_simple_tags("yi  ", script::YI);
424        // Unicode-5.0 additions.
425        test_simple_tags("nko ", script::NKO);
426        // Unicode-5.1 additions.
427        test_simple_tags("vai ", script::VAI);
428
429        // https://docs.microsoft.com/en-us/typography/opentype/spec/scripttags
430
431        // Unicode-5.2 additions.
432        test_simple_tags("mtei", script::MEETEI_MAYEK);
433        // Unicode-6.0 additions.
434        test_simple_tags("mand", script::MANDAIC);
435    }
436
437    macro_rules! test_script_from_language {
438        ($name:ident, $tag:expr, $lang:expr, $script:expr) => {
439            #[test]
440            fn $name() {
441                let tag = hb_tag_t::from_bytes_lossy($tag.as_bytes());
442                let (scripts, _) =
443                    tags_from_script_and_language($script, Language::new($lang).as_ref());
444                if !scripts.is_empty() {
445                    assert_eq!(scripts.as_slice(), &[tag]);
446                }
447            }
448        };
449    }
450
451    test_script_from_language!(script_from_language_01, "", "", None);
452    test_script_from_language!(script_from_language_02, "", "en", None);
453    test_script_from_language!(script_from_language_03, "copt", "en", Some(script::COPTIC));
454    test_script_from_language!(script_from_language_04, "", "x-hbsc", None);
455    test_script_from_language!(script_from_language_05, "copt", "x-hbsc", Some(script::COPTIC));
456    test_script_from_language!(script_from_language_06, "abc ", "x-hbscabc", None);
457    test_script_from_language!(script_from_language_07, "deva", "x-hbscdeva", None);
458    test_script_from_language!(script_from_language_08, "dev2", "x-hbscdev2", None);
459    test_script_from_language!(script_from_language_09, "dev3", "x-hbscdev3", None);
460    test_script_from_language!(script_from_language_10, "copt", "x-hbotpap0-hbsccopt", None);
461    test_script_from_language!(script_from_language_11, "", "en-x-hbsc", None);
462    test_script_from_language!(script_from_language_12, "copt", "en-x-hbsc", Some(script::COPTIC));
463    test_script_from_language!(script_from_language_13, "abc ", "en-x-hbscabc", None);
464    test_script_from_language!(script_from_language_14, "deva", "en-x-hbscdeva", None);
465    test_script_from_language!(script_from_language_15, "dev2", "en-x-hbscdev2", None);
466    test_script_from_language!(script_from_language_16, "dev3", "en-x-hbscdev3", None);
467    test_script_from_language!(script_from_language_17, "copt", "en-x-hbotpap0-hbsccopt", None);
468
469    #[test]
470    fn script_indic() {
471        fn check(tag1: &str, tag2: &str, tag3: &str, script: Script) {
472            let tag1 = hb_tag_t::from_bytes_lossy(tag1.as_bytes());
473            let tag2 = hb_tag_t::from_bytes_lossy(tag2.as_bytes());
474            let tag3 = hb_tag_t::from_bytes_lossy(tag3.as_bytes());
475
476            let (scripts, _) = tags_from_script_and_language(Some(script), None);
477            assert_eq!(scripts.as_slice(), &[tag1, tag2, tag3]);
478            assert_eq!(tag_to_script(tag1), Some(script));
479            assert_eq!(tag_to_script(tag2), Some(script));
480            assert_eq!(tag_to_script(tag3), Some(script));
481        }
482
483        check("bng3", "bng2", "beng", script::BENGALI);
484        check("dev3", "dev2", "deva", script::DEVANAGARI);
485        check("gjr3", "gjr2", "gujr", script::GUJARATI);
486        check("gur3", "gur2", "guru", script::GURMUKHI);
487        check("knd3", "knd2", "knda", script::KANNADA);
488        check("mlm3", "mlm2", "mlym", script::MALAYALAM);
489        check("ory3", "ory2", "orya", script::ORIYA);
490        check("tml3", "tml2", "taml", script::TAMIL);
491        check("tel3", "tel2", "telu", script::TELUGU);
492    }
493
494    // TODO: swap tag and lang
495    macro_rules! test_tag_from_language {
496        ($name:ident, $tag:expr, $lang:expr) => {
497            #[test]
498            fn $name() {
499                let tag = hb_tag_t::from_bytes_lossy($tag.as_bytes());
500                let (_, languages) = tags_from_script_and_language(
501                    None,
502                    Language::new(&$lang.to_lowercase()).as_ref(),
503                );
504                if !languages.is_empty() {
505                    assert_eq!(languages[0], tag);
506                }
507            }
508        };
509    }
510
511    test_tag_from_language!(tag_from_language_dflt, "dflt", "");
512    test_tag_from_language!(tag_from_language_ALT, "ALT", "alt");
513    test_tag_from_language!(tag_from_language_ARA, "ARA", "ar");
514    test_tag_from_language!(tag_from_language_AZE, "AZE", "az");
515    test_tag_from_language!(tag_from_language_az_ir, "AZE", "az-ir");
516    test_tag_from_language!(tag_from_language_az_az, "AZE", "az-az");
517    test_tag_from_language!(tag_from_language_ENG, "ENG", "en");
518    test_tag_from_language!(tag_from_language_en_US, "ENG", "en_US");
519    test_tag_from_language!(tag_from_language_CJA, "CJA", "cja"); /* Western Cham */
520    test_tag_from_language!(tag_from_language_CJM, "CJM", "cjm"); /* Eastern Cham */
521    test_tag_from_language!(tag_from_language_ENV, "EVN", "eve");
522    test_tag_from_language!(tag_from_language_HAL, "HAL", "cfm"); /* BCP47 and current ISO639-3 code for Halam/Falam Chin */
523    test_tag_from_language!(tag_from_language_flm, "HAL", "flm"); /* Retired ISO639-3 code for Halam/Falam Chin */
524    test_tag_from_language!(tag_from_language_hy, "HYE0", "hy");
525    test_tag_from_language!(tag_from_language_hyw, "HYE", "hyw");
526    test_tag_from_language!(tag_from_language_bgr, "QIN", "bgr"); /* Bawm Chin */
527    test_tag_from_language!(tag_from_language_cbl, "QIN", "cbl"); /* Bualkhaw Chin */
528    test_tag_from_language!(tag_from_language_cka, "QIN", "cka"); /* Khumi Awa Chin */
529    test_tag_from_language!(tag_from_language_cmr, "QIN", "cmr"); /* Mro-Khimi Chin */
530    test_tag_from_language!(tag_from_language_cnb, "QIN", "cnb"); /* Chinbon Chin */
531    test_tag_from_language!(tag_from_language_cnh, "QIN", "cnh"); /* Hakha Chin */
532    test_tag_from_language!(tag_from_language_cnk, "QIN", "cnk"); /* Khumi Chin */
533    test_tag_from_language!(tag_from_language_cnw, "QIN", "cnw"); /* Ngawn Chin */
534    test_tag_from_language!(tag_from_language_csh, "QIN", "csh"); /* Asho Chin */
535    test_tag_from_language!(tag_from_language_csy, "QIN", "csy"); /* Siyin Chin */
536    test_tag_from_language!(tag_from_language_ctd, "QIN", "ctd"); /* Tedim Chin */
537    test_tag_from_language!(tag_from_language_czt, "QIN", "czt"); /* Zotung Chin */
538    test_tag_from_language!(tag_from_language_dao, "QIN", "dao"); /* Daai Chin */
539    test_tag_from_language!(tag_from_language_htl, "QIN", "hlt"); /* Matu Chin */
540    test_tag_from_language!(tag_from_language_mrh, "QIN", "mrh"); /* Mara Chin */
541    test_tag_from_language!(tag_from_language_pck, "QIN", "pck"); /* Paite Chin */
542    test_tag_from_language!(tag_from_language_sez, "QIN", "sez"); /* Senthang Chin */
543    test_tag_from_language!(tag_from_language_tcp, "QIN", "tcp"); /* Tawr Chin */
544    test_tag_from_language!(tag_from_language_tcz, "QIN", "tcz"); /* Thado Chin */
545    test_tag_from_language!(tag_from_language_yos, "QIN", "yos"); /* Yos, deprecated by IANA in favor of Zou [zom] */
546    test_tag_from_language!(tag_from_language_zom, "QIN", "zom"); /* Zou */
547    test_tag_from_language!(tag_from_language_FAR, "FAR", "fa");
548    test_tag_from_language!(tag_from_language_fa_IR, "FAR", "fa_IR");
549    test_tag_from_language!(tag_from_language_man, "MNK", "man");
550    test_tag_from_language!(tag_from_language_SWA, "SWA", "aii"); /* Swadaya Aramaic */
551    test_tag_from_language!(tag_from_language_SYR, "SYR", "syr"); /* Syriac [macrolanguage] */
552    test_tag_from_language!(tag_from_language_amw, "SYR", "amw"); /* Western Neo-Aramaic */
553    test_tag_from_language!(tag_from_language_cld, "SYR", "cld"); /* Chaldean Neo-Aramaic */
554    test_tag_from_language!(tag_from_language_syc, "SYR", "syc"); /* Classical Syriac */
555    test_tag_from_language!(tag_from_language_TUA, "TUA", "tru"); /* Turoyo Aramaic */
556    test_tag_from_language!(tag_from_language_zh, "ZHS", "zh"); /* Chinese */
557    test_tag_from_language!(tag_from_language_zh_cn, "ZHS", "zh-cn"); /* Chinese (China) */
558    test_tag_from_language!(tag_from_language_zh_sg, "ZHS", "zh-sg"); /* Chinese (Singapore) */
559    test_tag_from_language!(tag_from_language_zh_mo, "ZHTM", "zh-mo"); /* Chinese (Macao) */
560    test_tag_from_language!(tag_from_language_zh_hant_mo, "ZHTM", "zh-hant-mo"); /* Chinese (Macao) */
561    test_tag_from_language!(tag_from_language_zh_hans_mo, "ZHS", "zh-hans-mo"); /* Chinese (Simplified, Macao) */
562    test_tag_from_language!(tag_from_language_ZHH, "ZHH", "zh-HK"); /* Chinese (Hong Kong) */
563    test_tag_from_language!(tag_from_language_zh_HanT_hK, "ZHH", "zH-HanT-hK"); /* Chinese (Hong Kong) */
564    test_tag_from_language!(tag_from_language_zh_HanS_hK, "ZHS", "zH-HanS-hK"); /* Chinese (Simplified, Hong Kong) */
565    test_tag_from_language!(tag_from_language_zh_tw, "ZHT", "zh-tw"); /* Chinese (Taiwan) */
566    test_tag_from_language!(tag_from_language_ZHS, "ZHS", "zh-Hans"); /* Chinese (Simplified) */
567    test_tag_from_language!(tag_from_language_ZHT, "ZHT", "zh-Hant"); /* Chinese (Traditional) */
568    test_tag_from_language!(tag_from_language_zh_xx, "ZHS", "zh-xx"); /* Chinese (Other) */
569    test_tag_from_language!(tag_from_language_zh_Hans_TW, "ZHS", "zh-Hans-TW");
570    test_tag_from_language!(tag_from_language_yue, "ZHH", "yue");
571    test_tag_from_language!(tag_from_language_yue_Hant, "ZHH", "yue-Hant");
572    test_tag_from_language!(tag_from_language_yue_Hans, "ZHS", "yue-Hans");
573    test_tag_from_language!(tag_from_language_ABC, "ABC", "abc");
574    test_tag_from_language!(tag_from_language_ABCD, "ABCD", "x-hbotabcd");
575    test_tag_from_language!(tag_from_language_asdf_asdf_wer_x_hbotabc_zxc, "ABC", "asdf-asdf-wer-x-hbotabc-zxc");
576    test_tag_from_language!(tag_from_language_asdf_asdf_wer_x_hbotabc, "ABC", "asdf-asdf-wer-x-hbotabc");
577    test_tag_from_language!(tag_from_language_asdf_asdf_wer_x_hbotabcd, "ABCD", "asdf-asdf-wer-x-hbotabcd");
578    test_tag_from_language!(tag_from_language_asdf_asdf_wer_x_hbot_zxc, "dflt", "asdf-asdf-wer-x-hbot-zxc");
579    test_tag_from_language!(tag_from_language_xy, "dflt", "xy");
580    test_tag_from_language!(tag_from_language_xyz, "XYZ", "xyz"); /* Unknown ISO 639-3 */
581    test_tag_from_language!(tag_from_language_xyz_qw, "XYZ", "xyz-qw"); /* Unknown ISO 639-3 */
582
583    /*
584     * Invalid input. The precise answer does not matter, as long as it
585     * does not crash or get into an infinite loop.
586     */
587    test_tag_from_language!(tag_from_language__fonipa, "IPPH", "-fonipa");
588
589    /*
590     * Tags that contain "-fonipa" as a substring but which do not contain
591     * the subtag "fonipa".
592     */
593    test_tag_from_language!(tag_from_language_en_fonipax, "ENG", "en-fonipax");
594    test_tag_from_language!(tag_from_language_en_x_fonipa, "ENG", "en-x-fonipa");
595    test_tag_from_language!(tag_from_language_en_a_fonipa, "ENG", "en-a-fonipa");
596    test_tag_from_language!(tag_from_language_en_a_qwe_b_fonipa, "ENG", "en-a-qwe-b-fonipa");
597
598    /* International Phonetic Alphabet */
599    test_tag_from_language!(tag_from_language_en_fonipa, "IPPH", "en-fonipa");
600    test_tag_from_language!(tag_from_language_en_fonipax_fonipa, "IPPH", "en-fonipax-fonipa");
601    test_tag_from_language!(tag_from_language_rm_ch_fonipa_sursilv_x_foobar, "IPPH", "rm-CH-fonipa-sursilv-x-foobar");
602    test_tag_from_language!(tag_from_language_IPPH, "IPPH", "und-fonipa");
603    test_tag_from_language!(tag_from_language_zh_fonipa, "IPPH", "zh-fonipa");
604
605    /* North American Phonetic Alphabet (Americanist Phonetic Notation) */
606    test_tag_from_language!(tag_from_language_en_fonnapa, "APPH", "en-fonnapa");
607    test_tag_from_language!(tag_from_language_chr_fonnapa, "APPH", "chr-fonnapa");
608    test_tag_from_language!(tag_from_language_APPH, "APPH", "und-fonnapa");
609
610    /* Khutsuri Georgian */
611    test_tag_from_language!(tag_from_language_ka_geok, "KGE", "ka-Geok");
612    test_tag_from_language!(tag_from_language_KGE, "KGE", "und-Geok");
613
614    /* Irish Traditional */
615    test_tag_from_language!(tag_from_language_IRT, "IRT", "ga-Latg");
616
617    /* Moldavian */
618    test_tag_from_language!(tag_from_language_MOL, "MOL", "ro-MD");
619
620    /* Polytonic Greek */
621    test_tag_from_language!(tag_from_language_PGR, "PGR", "el-polyton");
622    test_tag_from_language!(tag_from_language_el_CY_polyton, "PGR", "el-CY-polyton");
623
624    /* Estrangela Syriac */
625    test_tag_from_language!(tag_from_language_aii_Syre, "SYRE", "aii-Syre");
626    test_tag_from_language!(tag_from_language_de_Syre, "SYRE", "de-Syre");
627    test_tag_from_language!(tag_from_language_syr_Syre, "SYRE", "syr-Syre");
628    test_tag_from_language!(tag_from_language_und_Syre, "SYRE", "und-Syre");
629
630    /* Western Syriac */
631    test_tag_from_language!(tag_from_language_aii_Syrj, "SYRJ", "aii-Syrj");
632    test_tag_from_language!(tag_from_language_de_Syrj, "SYRJ", "de-Syrj");
633    test_tag_from_language!(tag_from_language_syr_Syrj, "SYRJ", "syr-Syrj");
634    test_tag_from_language!(tag_from_language_SYRJ, "SYRJ", "und-Syrj");
635
636    /* Eastern Syriac */
637    test_tag_from_language!(tag_from_language_aii_Syrn, "SYRN", "aii-Syrn");
638    test_tag_from_language!(tag_from_language_de_Syrn, "SYRN", "de-Syrn");
639    test_tag_from_language!(tag_from_language_syr_Syrn, "SYRN", "syr-Syrn");
640    test_tag_from_language!(tag_from_language_SYRN, "SYRN", "und-Syrn");
641
642    /* Test that x-hbot overrides the base language */
643    test_tag_from_language!(tag_from_language_fa_x_hbotabc_zxc, "ABC", "fa-x-hbotabc-zxc");
644    test_tag_from_language!(tag_from_language_fa_ir_x_hbotabc_zxc, "ABC", "fa-ir-x-hbotabc-zxc");
645    test_tag_from_language!(tag_from_language_zh_x_hbotabc_zxc, "ABC", "zh-x-hbotabc-zxc");
646    test_tag_from_language!(tag_from_language_zh_cn_x_hbotabc_zxc, "ABC", "zh-cn-x-hbotabc-zxc");
647    test_tag_from_language!(tag_from_language_zh_xy_x_hbotabc_zxc, "ABC", "zh-xy-x-hbotabc-zxc");
648    test_tag_from_language!(tag_from_language_xyz_xy_x_hbotabc_zxc, "ABC", "xyz-xy-x-hbotabc-zxc");
649
650    /* Unnormalized BCP 47 tags */
651    test_tag_from_language!(tag_from_language_ar_aao, "ARA", "ar-aao");
652    test_tag_from_language!(tag_from_language_art_lojban, "JBO", "art-lojban");
653    test_tag_from_language!(tag_from_language_kok_gom, "KOK", "kok-gom");
654    test_tag_from_language!(tag_from_language_i_lux, "LTZ", "i-lux");
655    test_tag_from_language!(tag_from_language_drh, "MNG", "drh");
656    test_tag_from_language!(tag_from_language_ar_ary1, "MOR", "ar-ary");
657    test_tag_from_language!(tag_from_language_ar_ary_DZ, "MOR", "ar-ary-DZ");
658    test_tag_from_language!(tag_from_language_no_bok, "NOR", "no-bok");
659    test_tag_from_language!(tag_from_language_no_nyn, "NYN", "no-nyn");
660    test_tag_from_language!(tag_from_language_i_hak, "ZHS", "i-hak");
661    test_tag_from_language!(tag_from_language_zh_guoyu, "ZHS", "zh-guoyu");
662    test_tag_from_language!(tag_from_language_zh_min, "ZHS", "zh-min");
663    test_tag_from_language!(tag_from_language_zh_min_nan, "ZHS", "zh-min-nan");
664    test_tag_from_language!(tag_from_language_zh_xiang, "ZHS", "zh-xiang");
665
666    /* BCP 47 tags that look similar to unrelated language system tags */
667    test_tag_from_language!(tag_from_language_als, "SQI", "als");
668    test_tag_from_language!(tag_from_language_far, "dflt", "far");
669
670    /* A UN M.49 region code, not an extended language subtag */
671    test_tag_from_language!(tag_from_language_ar_001, "ARA", "ar-001");
672
673    /* An invalid tag */
674    test_tag_from_language!(tag_from_language_invalid, "TRK", "tr@foo=bar");
675
676    macro_rules! test_tags {
677        ($name:ident, $script:expr, $lang:expr, $scripts:expr, $langs:expr) => {
678            #[test]
679            fn $name() {
680                let (scripts, languages) =
681                    tags_from_script_and_language($script, Language::new($lang).as_ref());
682
683                let exp_scripts: Vec<hb_tag_t> = $scripts.iter().map(|v| hb_tag_t::from_bytes_lossy(*v)).collect();
684                let exp_langs: Vec<hb_tag_t> = $langs.iter().map(|v| hb_tag_t::from_bytes_lossy(*v)).collect();
685
686                assert_eq!(exp_scripts, scripts.as_slice());
687                assert_eq!(exp_langs, languages.as_slice());
688            }
689        };
690    }
691
692    test_tags!(tag_full_en, None, "en", &[], &[b"ENG"]);
693    test_tags!(tag_full_en_x_hbscdflt, None, "en-x-hbscdflt", &[b"DFLT"], &[b"ENG"]);
694    test_tags!(tag_full_en_latin, Some(script::LATIN), "en", &[b"latn"], &[b"ENG"]);
695    test_tags!(tag_full_und_fonnapa, None, "und-fonnapa", &[], &[b"APPH"]);
696    test_tags!(tag_full_en_fonnapa, None, "en-fonnapa", &[], &[b"APPH"]);
697    test_tags!(tag_full_x_hbot1234_hbsc5678, None, "x-hbot1234-hbsc5678", &[b"5678"], &[b"1234"]);
698    test_tags!(tag_full_x_hbsc5678_hbot1234, None, "x-hbsc5678-hbot1234", &[b"5678"], &[b"1234"]);
699    test_tags!(tag_full_ml, Some(script::MALAYALAM), "ml", &[b"mlm3", b"mlm2", b"mlym"], &[b"MAL", b"MLR"]);
700    test_tags!(tag_full_xyz, None, "xyz", &[], &[b"XYZ"]);
701    test_tags!(tag_full_xy, None, "xy", &[], &[]);
702}