xref: /aosp_15_r20/external/fonttools/Tests/voltLib/parser_test.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1from fontTools.voltLib import ast
2from fontTools.voltLib.error import VoltLibError
3from fontTools.voltLib.parser import Parser
4from io import StringIO
5import unittest
6
7
8class ParserTest(unittest.TestCase):
9    def __init__(self, methodName):
10        unittest.TestCase.__init__(self, methodName)
11        # Python 3 renamed assertRaisesRegexp to assertRaisesRegex,
12        # and fires deprecation warnings if a program uses the old name.
13        if not hasattr(self, "assertRaisesRegex"):
14            self.assertRaisesRegex = self.assertRaisesRegexp
15
16    def assertSubEqual(self, sub, glyph_ref, replacement_ref):
17        glyphs = [[g.glyph for g in v] for v in sub.mapping.keys()]
18        replacement = [[g.glyph for g in v] for v in sub.mapping.values()]
19
20        self.assertEqual(glyphs, glyph_ref)
21        self.assertEqual(replacement, replacement_ref)
22
23    def test_def_glyph_base(self):
24        [def_glyph] = self.parse(
25            'DEF_GLYPH ".notdef" ID 0 TYPE BASE END_GLYPH'
26        ).statements
27        self.assertEqual(
28            (
29                def_glyph.name,
30                def_glyph.id,
31                def_glyph.unicode,
32                def_glyph.type,
33                def_glyph.components,
34            ),
35            (".notdef", 0, None, "BASE", None),
36        )
37
38    def test_def_glyph_base_with_unicode(self):
39        [def_glyph] = self.parse(
40            'DEF_GLYPH "space" ID 3 UNICODE 32 TYPE BASE END_GLYPH'
41        ).statements
42        self.assertEqual(
43            (
44                def_glyph.name,
45                def_glyph.id,
46                def_glyph.unicode,
47                def_glyph.type,
48                def_glyph.components,
49            ),
50            ("space", 3, [0x0020], "BASE", None),
51        )
52
53    def test_def_glyph_base_with_unicodevalues(self):
54        [def_glyph] = self.parse_(
55            'DEF_GLYPH "CR" ID 2 UNICODEVALUES "U+0009" ' "TYPE BASE END_GLYPH"
56        ).statements
57        self.assertEqual(
58            (
59                def_glyph.name,
60                def_glyph.id,
61                def_glyph.unicode,
62                def_glyph.type,
63                def_glyph.components,
64            ),
65            ("CR", 2, [0x0009], "BASE", None),
66        )
67
68    def test_def_glyph_base_with_mult_unicodevalues(self):
69        [def_glyph] = self.parse(
70            'DEF_GLYPH "CR" ID 2 UNICODEVALUES "U+0009,U+000D" ' "TYPE BASE END_GLYPH"
71        ).statements
72        self.assertEqual(
73            (
74                def_glyph.name,
75                def_glyph.id,
76                def_glyph.unicode,
77                def_glyph.type,
78                def_glyph.components,
79            ),
80            ("CR", 2, [0x0009, 0x000D], "BASE", None),
81        )
82
83    def test_def_glyph_base_with_empty_unicodevalues(self):
84        [def_glyph] = self.parse_(
85            'DEF_GLYPH "i.locl" ID 269 UNICODEVALUES "" ' "TYPE BASE END_GLYPH"
86        ).statements
87        self.assertEqual(
88            (
89                def_glyph.name,
90                def_glyph.id,
91                def_glyph.unicode,
92                def_glyph.type,
93                def_glyph.components,
94            ),
95            ("i.locl", 269, None, "BASE", None),
96        )
97
98    def test_def_glyph_base_2_components(self):
99        [def_glyph] = self.parse(
100            'DEF_GLYPH "glyphBase" ID 320 TYPE BASE COMPONENTS 2 END_GLYPH'
101        ).statements
102        self.assertEqual(
103            (
104                def_glyph.name,
105                def_glyph.id,
106                def_glyph.unicode,
107                def_glyph.type,
108                def_glyph.components,
109            ),
110            ("glyphBase", 320, None, "BASE", 2),
111        )
112
113    def test_def_glyph_ligature_2_components(self):
114        [def_glyph] = self.parse(
115            'DEF_GLYPH "f_f" ID 320 TYPE LIGATURE COMPONENTS 2 END_GLYPH'
116        ).statements
117        self.assertEqual(
118            (
119                def_glyph.name,
120                def_glyph.id,
121                def_glyph.unicode,
122                def_glyph.type,
123                def_glyph.components,
124            ),
125            ("f_f", 320, None, "LIGATURE", 2),
126        )
127
128    def test_def_glyph_mark(self):
129        [def_glyph] = self.parse(
130            'DEF_GLYPH "brevecomb" ID 320 TYPE MARK END_GLYPH'
131        ).statements
132        self.assertEqual(
133            (
134                def_glyph.name,
135                def_glyph.id,
136                def_glyph.unicode,
137                def_glyph.type,
138                def_glyph.components,
139            ),
140            ("brevecomb", 320, None, "MARK", None),
141        )
142
143    def test_def_glyph_component(self):
144        [def_glyph] = self.parse(
145            'DEF_GLYPH "f.f_f" ID 320 TYPE COMPONENT END_GLYPH'
146        ).statements
147        self.assertEqual(
148            (
149                def_glyph.name,
150                def_glyph.id,
151                def_glyph.unicode,
152                def_glyph.type,
153                def_glyph.components,
154            ),
155            ("f.f_f", 320, None, "COMPONENT", None),
156        )
157
158    def test_def_glyph_no_type(self):
159        [def_glyph] = self.parse('DEF_GLYPH "glyph20" ID 20 END_GLYPH').statements
160        self.assertEqual(
161            (
162                def_glyph.name,
163                def_glyph.id,
164                def_glyph.unicode,
165                def_glyph.type,
166                def_glyph.components,
167            ),
168            ("glyph20", 20, None, None, None),
169        )
170
171    def test_def_glyph_case_sensitive(self):
172        def_glyphs = self.parse(
173            'DEF_GLYPH "A" ID 3 UNICODE 65 TYPE BASE END_GLYPH\n'
174            'DEF_GLYPH "a" ID 4 UNICODE 97 TYPE BASE END_GLYPH'
175        ).statements
176        self.assertEqual(
177            (
178                def_glyphs[0].name,
179                def_glyphs[0].id,
180                def_glyphs[0].unicode,
181                def_glyphs[0].type,
182                def_glyphs[0].components,
183            ),
184            ("A", 3, [0x41], "BASE", None),
185        )
186        self.assertEqual(
187            (
188                def_glyphs[1].name,
189                def_glyphs[1].id,
190                def_glyphs[1].unicode,
191                def_glyphs[1].type,
192                def_glyphs[1].components,
193            ),
194            ("a", 4, [0x61], "BASE", None),
195        )
196
197    def test_def_group_glyphs(self):
198        [def_group] = self.parse(
199            'DEF_GROUP "aaccented"\n'
200            ' ENUM GLYPH "aacute" GLYPH "abreve" GLYPH "acircumflex" '
201            'GLYPH "adieresis" GLYPH "ae" GLYPH "agrave" GLYPH "amacron" '
202            'GLYPH "aogonek" GLYPH "aring" GLYPH "atilde" END_ENUM\n'
203            "END_GROUP"
204        ).statements
205        self.assertEqual(
206            (def_group.name, def_group.enum.glyphSet()),
207            (
208                "aaccented",
209                (
210                    "aacute",
211                    "abreve",
212                    "acircumflex",
213                    "adieresis",
214                    "ae",
215                    "agrave",
216                    "amacron",
217                    "aogonek",
218                    "aring",
219                    "atilde",
220                ),
221            ),
222        )
223
224    def test_def_group_groups(self):
225        [group1, group2, test_group] = self.parse(
226            'DEF_GROUP "Group1"\n'
227            ' ENUM GLYPH "a" GLYPH "b" GLYPH "c" GLYPH "d" END_ENUM\n'
228            "END_GROUP\n"
229            'DEF_GROUP "Group2"\n'
230            ' ENUM GLYPH "e" GLYPH "f" GLYPH "g" GLYPH "h" END_ENUM\n'
231            "END_GROUP\n"
232            'DEF_GROUP "TestGroup"\n'
233            ' ENUM GROUP "Group1" GROUP "Group2" END_ENUM\n'
234            "END_GROUP"
235        ).statements
236        groups = [g.group for g in test_group.enum.enum]
237        self.assertEqual((test_group.name, groups), ("TestGroup", ["Group1", "Group2"]))
238
239    def test_def_group_groups_not_yet_defined(self):
240        [group1, test_group1, test_group2, test_group3, group2] = self.parse(
241            'DEF_GROUP "Group1"\n'
242            ' ENUM GLYPH "a" GLYPH "b" GLYPH "c" GLYPH "d" END_ENUM\n'
243            "END_GROUP\n"
244            'DEF_GROUP "TestGroup1"\n'
245            ' ENUM GROUP "Group1" GROUP "Group2" END_ENUM\n'
246            "END_GROUP\n"
247            'DEF_GROUP "TestGroup2"\n'
248            ' ENUM GROUP "Group2" END_ENUM\n'
249            "END_GROUP\n"
250            'DEF_GROUP "TestGroup3"\n'
251            ' ENUM GROUP "Group2" GROUP "Group1" END_ENUM\n'
252            "END_GROUP\n"
253            'DEF_GROUP "Group2"\n'
254            ' ENUM GLYPH "e" GLYPH "f" GLYPH "g" GLYPH "h" END_ENUM\n'
255            "END_GROUP"
256        ).statements
257        groups = [g.group for g in test_group1.enum.enum]
258        self.assertEqual(
259            (test_group1.name, groups), ("TestGroup1", ["Group1", "Group2"])
260        )
261        groups = [g.group for g in test_group2.enum.enum]
262        self.assertEqual((test_group2.name, groups), ("TestGroup2", ["Group2"]))
263        groups = [g.group for g in test_group3.enum.enum]
264        self.assertEqual(
265            (test_group3.name, groups), ("TestGroup3", ["Group2", "Group1"])
266        )
267
268    # def test_def_group_groups_undefined(self):
269    #     with self.assertRaisesRegex(
270    #             VoltLibError,
271    #             r'Group "Group2" is used but undefined.'):
272    #         [group1, test_group, group2] = self.parse(
273    #             'DEF_GROUP "Group1"\n'
274    #             'ENUM GLYPH "a" GLYPH "b" GLYPH "c" GLYPH "d" END_ENUM\n'
275    #             'END_GROUP\n'
276    #             'DEF_GROUP "TestGroup"\n'
277    #             'ENUM GROUP "Group1" GROUP "Group2" END_ENUM\n'
278    #             'END_GROUP\n'
279    #         ).statements
280
281    def test_def_group_glyphs_and_group(self):
282        [def_group1, def_group2] = self.parse(
283            'DEF_GROUP "aaccented"\n'
284            ' ENUM GLYPH "aacute" GLYPH "abreve" GLYPH "acircumflex" '
285            'GLYPH "adieresis" GLYPH "ae" GLYPH "agrave" GLYPH "amacron" '
286            'GLYPH "aogonek" GLYPH "aring" GLYPH "atilde" END_ENUM\n'
287            "END_GROUP\n"
288            'DEF_GROUP "KERN_lc_a_2ND"\n'
289            ' ENUM GLYPH "a" GROUP "aaccented" END_ENUM\n'
290            "END_GROUP"
291        ).statements
292        items = def_group2.enum.enum
293        self.assertEqual(
294            (def_group2.name, items[0].glyphSet(), items[1].group),
295            ("KERN_lc_a_2ND", ("a",), "aaccented"),
296        )
297
298    def test_def_group_range(self):
299        def_group = self.parse(
300            'DEF_GLYPH "a" ID 163 UNICODE 97 TYPE BASE END_GLYPH\n'
301            'DEF_GLYPH "agrave" ID 194 UNICODE 224 TYPE BASE END_GLYPH\n'
302            'DEF_GLYPH "aacute" ID 195 UNICODE 225 TYPE BASE END_GLYPH\n'
303            'DEF_GLYPH "acircumflex" ID 196 UNICODE 226 TYPE BASE END_GLYPH\n'
304            'DEF_GLYPH "atilde" ID 197 UNICODE 227 TYPE BASE END_GLYPH\n'
305            'DEF_GLYPH "c" ID 165 UNICODE 99 TYPE BASE END_GLYPH\n'
306            'DEF_GLYPH "ccaron" ID 209 UNICODE 269 TYPE BASE END_GLYPH\n'
307            'DEF_GLYPH "ccedilla" ID 210 UNICODE 231 TYPE BASE END_GLYPH\n'
308            'DEF_GLYPH "cdotaccent" ID 210 UNICODE 267 TYPE BASE END_GLYPH\n'
309            'DEF_GROUP "KERN_lc_a_2ND"\n'
310            ' ENUM RANGE "a" TO "atilde" GLYPH "b" RANGE "c" TO "cdotaccent" '
311            "END_ENUM\n"
312            "END_GROUP"
313        ).statements[-1]
314        self.assertEqual(
315            (def_group.name, def_group.enum.glyphSet()),
316            (
317                "KERN_lc_a_2ND",
318                (
319                    "a",
320                    "agrave",
321                    "aacute",
322                    "acircumflex",
323                    "atilde",
324                    "b",
325                    "c",
326                    "ccaron",
327                    "ccedilla",
328                    "cdotaccent",
329                ),
330            ),
331        )
332
333    def test_group_duplicate(self):
334        self.assertRaisesRegex(
335            VoltLibError,
336            'Glyph group "dupe" already defined, ' "group names are case insensitive",
337            self.parse,
338            'DEF_GROUP "dupe"\n'
339            'ENUM GLYPH "a" GLYPH "b" END_ENUM\n'
340            "END_GROUP\n"
341            'DEF_GROUP "dupe"\n'
342            'ENUM GLYPH "x" END_ENUM\n'
343            "END_GROUP",
344        )
345
346    def test_group_duplicate_case_insensitive(self):
347        self.assertRaisesRegex(
348            VoltLibError,
349            'Glyph group "Dupe" already defined, ' "group names are case insensitive",
350            self.parse,
351            'DEF_GROUP "dupe"\n'
352            'ENUM GLYPH "a" GLYPH "b" END_ENUM\n'
353            "END_GROUP\n"
354            'DEF_GROUP "Dupe"\n'
355            'ENUM GLYPH "x" END_ENUM\n'
356            "END_GROUP",
357        )
358
359    def test_script_without_langsys(self):
360        [script] = self.parse(
361            'DEF_SCRIPT NAME "Latin" TAG "latn"\n\n' "END_SCRIPT"
362        ).statements
363        self.assertEqual((script.name, script.tag, script.langs), ("Latin", "latn", []))
364
365    def test_langsys_normal(self):
366        [def_script] = self.parse(
367            'DEF_SCRIPT NAME "Latin" TAG "latn"\n\n'
368            'DEF_LANGSYS NAME "Romanian" TAG "ROM "\n\n'
369            "END_LANGSYS\n"
370            'DEF_LANGSYS NAME "Moldavian" TAG "MOL "\n\n'
371            "END_LANGSYS\n"
372            "END_SCRIPT"
373        ).statements
374        self.assertEqual((def_script.name, def_script.tag), ("Latin", "latn"))
375        def_lang = def_script.langs[0]
376        self.assertEqual((def_lang.name, def_lang.tag), ("Romanian", "ROM "))
377        def_lang = def_script.langs[1]
378        self.assertEqual((def_lang.name, def_lang.tag), ("Moldavian", "MOL "))
379
380    def test_langsys_no_script_name(self):
381        [langsys] = self.parse(
382            'DEF_SCRIPT TAG "latn"\n\n'
383            'DEF_LANGSYS NAME "Default" TAG "dflt"\n\n'
384            "END_LANGSYS\n"
385            "END_SCRIPT"
386        ).statements
387        self.assertEqual((langsys.name, langsys.tag), (None, "latn"))
388        lang = langsys.langs[0]
389        self.assertEqual((lang.name, lang.tag), ("Default", "dflt"))
390
391    def test_langsys_no_script_tag_fails(self):
392        with self.assertRaisesRegex(VoltLibError, r'.*Expected "TAG"'):
393            [langsys] = self.parse(
394                'DEF_SCRIPT NAME "Latin"\n\n'
395                'DEF_LANGSYS NAME "Default" TAG "dflt"\n\n'
396                "END_LANGSYS\n"
397                "END_SCRIPT"
398            ).statements
399
400    def test_langsys_duplicate_script(self):
401        with self.assertRaisesRegex(
402            VoltLibError,
403            'Script "DFLT" already defined, ' "script tags are case insensitive",
404        ):
405            [langsys1, langsys2] = self.parse(
406                'DEF_SCRIPT NAME "Default" TAG "DFLT"\n\n'
407                'DEF_LANGSYS NAME "Default" TAG "dflt"\n\n'
408                "END_LANGSYS\n"
409                "END_SCRIPT\n"
410                'DEF_SCRIPT TAG "DFLT"\n\n'
411                'DEF_LANGSYS NAME "Default" TAG "dflt"\n\n'
412                "END_LANGSYS\n"
413                "END_SCRIPT"
414            ).statements
415
416    def test_langsys_duplicate_lang(self):
417        with self.assertRaisesRegex(
418            VoltLibError,
419            'Language "dflt" already defined in script "DFLT", '
420            "language tags are case insensitive",
421        ):
422            [langsys] = self.parse(
423                'DEF_SCRIPT NAME "Default" TAG "DFLT"\n'
424                'DEF_LANGSYS NAME "Default" TAG "dflt"\n'
425                "END_LANGSYS\n"
426                'DEF_LANGSYS NAME "Default" TAG "dflt"\n'
427                "END_LANGSYS\n"
428                "END_SCRIPT"
429            ).statements
430
431    def test_langsys_lang_in_separate_scripts(self):
432        [langsys1, langsys2] = self.parse(
433            'DEF_SCRIPT NAME "Default" TAG "DFLT"\n\n'
434            'DEF_LANGSYS NAME "Default" TAG "dflt"\n\n'
435            "END_LANGSYS\n"
436            'DEF_LANGSYS NAME "Default" TAG "ROM "\n\n'
437            "END_LANGSYS\n"
438            "END_SCRIPT\n"
439            'DEF_SCRIPT NAME "Latin" TAG "latn"\n\n'
440            'DEF_LANGSYS NAME "Default" TAG "dflt"\n\n'
441            "END_LANGSYS\n"
442            'DEF_LANGSYS NAME "Default" TAG "ROM "\n\n'
443            "END_LANGSYS\n"
444            "END_SCRIPT"
445        ).statements
446        self.assertEqual(
447            (langsys1.langs[0].tag, langsys1.langs[1].tag), ("dflt", "ROM ")
448        )
449        self.assertEqual(
450            (langsys2.langs[0].tag, langsys2.langs[1].tag), ("dflt", "ROM ")
451        )
452
453    def test_langsys_no_lang_name(self):
454        [langsys] = self.parse(
455            'DEF_SCRIPT NAME "Latin" TAG "latn"\n\n'
456            'DEF_LANGSYS TAG "dflt"\n\n'
457            "END_LANGSYS\n"
458            "END_SCRIPT"
459        ).statements
460        self.assertEqual((langsys.name, langsys.tag), ("Latin", "latn"))
461        lang = langsys.langs[0]
462        self.assertEqual((lang.name, lang.tag), (None, "dflt"))
463
464    def test_langsys_no_langsys_tag_fails(self):
465        with self.assertRaisesRegex(VoltLibError, r'.*Expected "TAG"'):
466            [langsys] = self.parse(
467                'DEF_SCRIPT NAME "Latin" TAG "latn"\n\n'
468                'DEF_LANGSYS NAME "Default"\n\n'
469                "END_LANGSYS\n"
470                "END_SCRIPT"
471            ).statements
472
473    def test_feature(self):
474        [def_script] = self.parse(
475            'DEF_SCRIPT NAME "Latin" TAG "latn"\n\n'
476            'DEF_LANGSYS NAME "Romanian" TAG "ROM "\n\n'
477            'DEF_FEATURE NAME "Fractions" TAG "frac"\n'
478            ' LOOKUP "fraclookup"\n'
479            "END_FEATURE\n"
480            "END_LANGSYS\n"
481            "END_SCRIPT"
482        ).statements
483        def_feature = def_script.langs[0].features[0]
484        self.assertEqual(
485            (def_feature.name, def_feature.tag, def_feature.lookups),
486            ("Fractions", "frac", ["fraclookup"]),
487        )
488        [def_script] = self.parse(
489            'DEF_SCRIPT NAME "Latin" TAG "latn"\n\n'
490            'DEF_LANGSYS NAME "Romanian" TAG "ROM "\n\n'
491            'DEF_FEATURE NAME "Kerning" TAG "kern"\n'
492            ' LOOKUP "kern1" LOOKUP "kern2"\n'
493            "END_FEATURE\n"
494            "END_LANGSYS\n"
495            "END_SCRIPT"
496        ).statements
497        def_feature = def_script.langs[0].features[0]
498        self.assertEqual(
499            (def_feature.name, def_feature.tag, def_feature.lookups),
500            ("Kerning", "kern", ["kern1", "kern2"]),
501        )
502
503    def test_lookup_duplicate(self):
504        with self.assertRaisesRegex(
505            VoltLibError,
506            'Lookup "dupe" already defined, ' "lookup names are case insensitive",
507        ):
508            [lookup1, lookup2] = self.parse(
509                'DEF_LOOKUP "dupe"\n'
510                "AS_SUBSTITUTION\n"
511                'SUB GLYPH "a"\n'
512                'WITH GLYPH "a.alt"\n'
513                "END_SUB\n"
514                "END_SUBSTITUTION\n"
515                'DEF_LOOKUP "dupe"\n'
516                "AS_SUBSTITUTION\n"
517                'SUB GLYPH "b"\n'
518                'WITH GLYPH "b.alt"\n'
519                "END_SUB\n"
520                "END_SUBSTITUTION\n"
521            ).statements
522
523    def test_lookup_duplicate_insensitive_case(self):
524        with self.assertRaisesRegex(
525            VoltLibError,
526            'Lookup "Dupe" already defined, ' "lookup names are case insensitive",
527        ):
528            [lookup1, lookup2] = self.parse(
529                'DEF_LOOKUP "dupe"\n'
530                "AS_SUBSTITUTION\n"
531                'SUB GLYPH "a"\n'
532                'WITH GLYPH "a.alt"\n'
533                "END_SUB\n"
534                "END_SUBSTITUTION\n"
535                'DEF_LOOKUP "Dupe"\n'
536                "AS_SUBSTITUTION\n"
537                'SUB GLYPH "b"\n'
538                'WITH GLYPH "b.alt"\n'
539                "END_SUB\n"
540                "END_SUBSTITUTION\n"
541            ).statements
542
543    def test_lookup_name_starts_with_letter(self):
544        with self.assertRaisesRegex(
545            VoltLibError, r'Lookup name "\\lookupname" must start with a letter'
546        ):
547            [lookup] = self.parse(
548                'DEF_LOOKUP "\\lookupname"\n'
549                "AS_SUBSTITUTION\n"
550                'SUB GLYPH "a"\n'
551                'WITH GLYPH "a.alt"\n'
552                "END_SUB\n"
553                "END_SUBSTITUTION\n"
554            ).statements
555
556    def test_lookup_comments(self):
557        [lookup] = self.parse(
558            'DEF_LOOKUP "test" PROCESS_BASE PROCESS_MARKS ALL DIRECTION LTR\n'
559            'COMMENTS "Hello\\nWorld"\n'
560            "IN_CONTEXT\n"
561            "END_CONTEXT\n"
562            "AS_SUBSTITUTION\n"
563            'SUB GLYPH "a"\n'
564            'WITH GLYPH "b"\n'
565            "END_SUB\n"
566            "END_SUBSTITUTION"
567        ).statements
568        self.assertEqual(lookup.name, "test")
569        self.assertEqual(lookup.comments, "Hello\nWorld")
570
571    def test_substitution_empty(self):
572        with self.assertRaisesRegex(VoltLibError, r"Expected SUB"):
573            [lookup] = self.parse(
574                'DEF_LOOKUP "empty_substitution" PROCESS_BASE PROCESS_MARKS '
575                "ALL DIRECTION LTR\n"
576                "IN_CONTEXT\n"
577                "END_CONTEXT\n"
578                "AS_SUBSTITUTION\n"
579                "END_SUBSTITUTION"
580            ).statements
581
582    def test_substitution_invalid_many_to_many(self):
583        with self.assertRaisesRegex(VoltLibError, r"Invalid substitution type"):
584            [lookup] = self.parse(
585                'DEF_LOOKUP "invalid_substitution" PROCESS_BASE PROCESS_MARKS '
586                "ALL DIRECTION LTR\n"
587                "IN_CONTEXT\n"
588                "END_CONTEXT\n"
589                "AS_SUBSTITUTION\n"
590                'SUB GLYPH "f" GLYPH "i"\n'
591                'WITH GLYPH "f.alt" GLYPH "i.alt"\n'
592                "END_SUB\n"
593                "END_SUBSTITUTION"
594            ).statements
595
596    def test_substitution_invalid_reverse_chaining_single(self):
597        with self.assertRaisesRegex(VoltLibError, r"Invalid substitution type"):
598            [lookup] = self.parse(
599                'DEF_LOOKUP "invalid_substitution" PROCESS_BASE PROCESS_MARKS '
600                "ALL DIRECTION LTR REVERSAL\n"
601                "IN_CONTEXT\n"
602                "END_CONTEXT\n"
603                "AS_SUBSTITUTION\n"
604                'SUB GLYPH "f" GLYPH "i"\n'
605                'WITH GLYPH "f_i"\n'
606                "END_SUB\n"
607                "END_SUBSTITUTION"
608            ).statements
609
610    def test_substitution_invalid_mixed(self):
611        with self.assertRaisesRegex(VoltLibError, r"Invalid substitution type"):
612            [lookup] = self.parse(
613                'DEF_LOOKUP "invalid_substitution" PROCESS_BASE PROCESS_MARKS '
614                "ALL DIRECTION LTR\n"
615                "IN_CONTEXT\n"
616                "END_CONTEXT\n"
617                "AS_SUBSTITUTION\n"
618                'SUB GLYPH "fi"\n'
619                'WITH GLYPH "f" GLYPH "i"\n'
620                "END_SUB\n"
621                'SUB GLYPH "f" GLYPH "l"\n'
622                'WITH GLYPH "f_l"\n'
623                "END_SUB\n"
624                "END_SUBSTITUTION"
625            ).statements
626
627    def test_substitution_single(self):
628        [lookup] = self.parse(
629            'DEF_LOOKUP "smcp" PROCESS_BASE PROCESS_MARKS ALL '
630            "DIRECTION LTR\n"
631            "IN_CONTEXT\n"
632            "END_CONTEXT\n"
633            "AS_SUBSTITUTION\n"
634            'SUB GLYPH "a"\n'
635            'WITH GLYPH "a.sc"\n'
636            "END_SUB\n"
637            'SUB GLYPH "b"\n'
638            'WITH GLYPH "b.sc"\n'
639            "END_SUB\n"
640            "END_SUBSTITUTION"
641        ).statements
642        self.assertEqual(lookup.name, "smcp")
643        self.assertSubEqual(lookup.sub, [["a"], ["b"]], [["a.sc"], ["b.sc"]])
644
645    def test_substitution_single_in_context(self):
646        [group, lookup] = self.parse(
647            'DEF_GROUP "Denominators"\n'
648            ' ENUM GLYPH "one.dnom" GLYPH "two.dnom" END_ENUM\n'
649            "END_GROUP\n"
650            'DEF_LOOKUP "fracdnom" PROCESS_BASE PROCESS_MARKS ALL '
651            "DIRECTION LTR\n"
652            "IN_CONTEXT\n"
653            ' LEFT ENUM GROUP "Denominators" GLYPH "fraction" END_ENUM\n'
654            "END_CONTEXT\n"
655            "AS_SUBSTITUTION\n"
656            'SUB GLYPH "one"\n'
657            'WITH GLYPH "one.dnom"\n'
658            "END_SUB\n"
659            'SUB GLYPH "two"\n'
660            'WITH GLYPH "two.dnom"\n'
661            "END_SUB\n"
662            "END_SUBSTITUTION"
663        ).statements
664        context = lookup.context[0]
665
666        self.assertEqual(lookup.name, "fracdnom")
667        self.assertEqual(context.ex_or_in, "IN_CONTEXT")
668        self.assertEqual(len(context.left), 1)
669        self.assertEqual(len(context.left[0]), 1)
670        self.assertEqual(len(context.left[0][0].enum), 2)
671        self.assertEqual(context.left[0][0].enum[0].group, "Denominators")
672        self.assertEqual(context.left[0][0].enum[1].glyph, "fraction")
673        self.assertEqual(context.right, [])
674        self.assertSubEqual(
675            lookup.sub, [["one"], ["two"]], [["one.dnom"], ["two.dnom"]]
676        )
677
678    def test_substitution_single_in_contexts(self):
679        [group, lookup] = self.parse(
680            'DEF_GROUP "Hebrew"\n'
681            ' ENUM GLYPH "uni05D0" GLYPH "uni05D1" END_ENUM\n'
682            "END_GROUP\n"
683            'DEF_LOOKUP "HebrewCurrency" PROCESS_BASE PROCESS_MARKS ALL '
684            "DIRECTION LTR\n"
685            "IN_CONTEXT\n"
686            ' RIGHT GROUP "Hebrew"\n'
687            ' RIGHT GLYPH "one.Hebr"\n'
688            "END_CONTEXT\n"
689            "IN_CONTEXT\n"
690            ' LEFT GROUP "Hebrew"\n'
691            ' LEFT GLYPH "one.Hebr"\n'
692            "END_CONTEXT\n"
693            "AS_SUBSTITUTION\n"
694            'SUB GLYPH "dollar"\n'
695            'WITH GLYPH "dollar.Hebr"\n'
696            "END_SUB\n"
697            "END_SUBSTITUTION"
698        ).statements
699        context1 = lookup.context[0]
700        context2 = lookup.context[1]
701
702        self.assertEqual(lookup.name, "HebrewCurrency")
703
704        self.assertEqual(context1.ex_or_in, "IN_CONTEXT")
705        self.assertEqual(context1.left, [])
706        self.assertEqual(len(context1.right), 2)
707        self.assertEqual(len(context1.right[0]), 1)
708        self.assertEqual(len(context1.right[1]), 1)
709        self.assertEqual(context1.right[0][0].group, "Hebrew")
710        self.assertEqual(context1.right[1][0].glyph, "one.Hebr")
711
712        self.assertEqual(context2.ex_or_in, "IN_CONTEXT")
713        self.assertEqual(len(context2.left), 2)
714        self.assertEqual(len(context2.left[0]), 1)
715        self.assertEqual(len(context2.left[1]), 1)
716        self.assertEqual(context2.left[0][0].group, "Hebrew")
717        self.assertEqual(context2.left[1][0].glyph, "one.Hebr")
718        self.assertEqual(context2.right, [])
719
720    def test_substitution_skip_base(self):
721        [group, lookup] = self.parse(
722            'DEF_GROUP "SomeMarks"\n'
723            ' ENUM GLYPH "marka" GLYPH "markb" END_ENUM\n'
724            "END_GROUP\n"
725            'DEF_LOOKUP "SomeSub" SKIP_BASE PROCESS_MARKS ALL '
726            "DIRECTION LTR\n"
727            "IN_CONTEXT\n"
728            "END_CONTEXT\n"
729            "AS_SUBSTITUTION\n"
730            'SUB GLYPH "A"\n'
731            'WITH GLYPH "A.c2sc"\n'
732            "END_SUB\n"
733            "END_SUBSTITUTION"
734        ).statements
735        self.assertEqual((lookup.name, lookup.process_base), ("SomeSub", False))
736
737    def test_substitution_process_base(self):
738        [group, lookup] = self.parse(
739            'DEF_GROUP "SomeMarks"\n'
740            ' ENUM GLYPH "marka" GLYPH "markb" END_ENUM\n'
741            "END_GROUP\n"
742            'DEF_LOOKUP "SomeSub" PROCESS_BASE PROCESS_MARKS ALL '
743            "DIRECTION LTR\n"
744            "IN_CONTEXT\n"
745            "END_CONTEXT\n"
746            "AS_SUBSTITUTION\n"
747            'SUB GLYPH "A"\n'
748            'WITH GLYPH "A.c2sc"\n'
749            "END_SUB\n"
750            "END_SUBSTITUTION"
751        ).statements
752        self.assertEqual((lookup.name, lookup.process_base), ("SomeSub", True))
753
754    def test_substitution_process_marks(self):
755        [group, lookup] = self.parse(
756            'DEF_GROUP "SomeMarks"\n'
757            ' ENUM GLYPH "marka" GLYPH "markb" END_ENUM\n'
758            "END_GROUP\n"
759            'DEF_LOOKUP "SomeSub" PROCESS_BASE PROCESS_MARKS "SomeMarks"\n'
760            "IN_CONTEXT\n"
761            "END_CONTEXT\n"
762            "AS_SUBSTITUTION\n"
763            'SUB GLYPH "A"\n'
764            'WITH GLYPH "A.c2sc"\n'
765            "END_SUB\n"
766            "END_SUBSTITUTION"
767        ).statements
768        self.assertEqual((lookup.name, lookup.process_marks), ("SomeSub", "SomeMarks"))
769
770    def test_substitution_process_marks_all(self):
771        [lookup] = self.parse(
772            'DEF_LOOKUP "SomeSub" PROCESS_BASE PROCESS_MARKS ALL\n'
773            "IN_CONTEXT\n"
774            "END_CONTEXT\n"
775            "AS_SUBSTITUTION\n"
776            'SUB GLYPH "A"\n'
777            'WITH GLYPH "A.c2sc"\n'
778            "END_SUB\n"
779            "END_SUBSTITUTION"
780        ).statements
781        self.assertEqual((lookup.name, lookup.process_marks), ("SomeSub", True))
782
783    def test_substitution_process_marks_none(self):
784        [lookup] = self.parse_(
785            'DEF_LOOKUP "SomeSub" PROCESS_BASE PROCESS_MARKS "NONE"\n'
786            "IN_CONTEXT\n"
787            "END_CONTEXT\n"
788            "AS_SUBSTITUTION\n"
789            'SUB GLYPH "A"\n'
790            'WITH GLYPH "A.c2sc"\n'
791            "END_SUB\n"
792            "END_SUBSTITUTION"
793        ).statements
794        self.assertEqual((lookup.name, lookup.process_marks), ("SomeSub", False))
795
796    def test_substitution_process_marks_bad(self):
797        with self.assertRaisesRegex(
798            VoltLibError, "Expected ALL, NONE, MARK_GLYPH_SET or an ID"
799        ):
800            self.parse(
801                'DEF_GROUP "SomeMarks" ENUM GLYPH "marka" GLYPH "markb" '
802                "END_ENUM END_GROUP\n"
803                'DEF_LOOKUP "SomeSub" PROCESS_BASE PROCESS_MARKS SomeMarks '
804                "AS_SUBSTITUTION\n"
805                'SUB GLYPH "A" WITH GLYPH "A.c2sc"\n'
806                "END_SUB\n"
807                "END_SUBSTITUTION"
808            )
809
810    def test_substitution_skip_marks(self):
811        [group, lookup] = self.parse(
812            'DEF_GROUP "SomeMarks"\n'
813            ' ENUM GLYPH "marka" GLYPH "markb" END_ENUM\n'
814            "END_GROUP\n"
815            'DEF_LOOKUP "SomeSub" PROCESS_BASE SKIP_MARKS DIRECTION LTR\n'
816            "IN_CONTEXT\n"
817            "END_CONTEXT\n"
818            "AS_SUBSTITUTION\n"
819            'SUB GLYPH "A"\n'
820            'WITH GLYPH "A.c2sc"\n'
821            "END_SUB\n"
822            "END_SUBSTITUTION"
823        ).statements
824        self.assertEqual((lookup.name, lookup.process_marks), ("SomeSub", False))
825
826    def test_substitution_mark_attachment(self):
827        [group, lookup] = self.parse(
828            'DEF_GROUP "SomeMarks"\n'
829            ' ENUM GLYPH "acutecmb" GLYPH "gravecmb" END_ENUM\n'
830            "END_GROUP\n"
831            'DEF_LOOKUP "SomeSub" PROCESS_BASE '
832            'PROCESS_MARKS "SomeMarks" DIRECTION RTL\n'
833            "IN_CONTEXT\n"
834            "END_CONTEXT\n"
835            "AS_SUBSTITUTION\n"
836            'SUB GLYPH "A"\n'
837            'WITH GLYPH "A.c2sc"\n'
838            "END_SUB\n"
839            "END_SUBSTITUTION"
840        ).statements
841        self.assertEqual((lookup.name, lookup.process_marks), ("SomeSub", "SomeMarks"))
842
843    def test_substitution_mark_glyph_set(self):
844        [group, lookup] = self.parse(
845            'DEF_GROUP "SomeMarks"\n'
846            ' ENUM GLYPH "acutecmb" GLYPH "gravecmb" END_ENUM\n'
847            "END_GROUP\n"
848            'DEF_LOOKUP "SomeSub" PROCESS_BASE '
849            'PROCESS_MARKS MARK_GLYPH_SET "SomeMarks" DIRECTION RTL\n'
850            "IN_CONTEXT\n"
851            "END_CONTEXT\n"
852            "AS_SUBSTITUTION\n"
853            'SUB GLYPH "A"\n'
854            'WITH GLYPH "A.c2sc"\n'
855            "END_SUB\n"
856            "END_SUBSTITUTION"
857        ).statements
858        self.assertEqual((lookup.name, lookup.mark_glyph_set), ("SomeSub", "SomeMarks"))
859
860    def test_substitution_process_all_marks(self):
861        [group, lookup] = self.parse(
862            'DEF_GROUP "SomeMarks"\n'
863            ' ENUM GLYPH "acutecmb" GLYPH "gravecmb" END_ENUM\n'
864            "END_GROUP\n"
865            'DEF_LOOKUP "SomeSub" PROCESS_BASE PROCESS_MARKS ALL '
866            "DIRECTION RTL\n"
867            "IN_CONTEXT\n"
868            "END_CONTEXT\n"
869            "AS_SUBSTITUTION\n"
870            'SUB GLYPH "A"\n'
871            'WITH GLYPH "A.c2sc"\n'
872            "END_SUB\n"
873            "END_SUBSTITUTION"
874        ).statements
875        self.assertEqual((lookup.name, lookup.process_marks), ("SomeSub", True))
876
877    def test_substitution_no_reversal(self):
878        # TODO: check right context with no reversal
879        [lookup] = self.parse(
880            'DEF_LOOKUP "Lookup" PROCESS_BASE PROCESS_MARKS ALL '
881            "DIRECTION LTR\n"
882            "IN_CONTEXT\n"
883            ' RIGHT ENUM GLYPH "a" GLYPH "b" END_ENUM\n'
884            "END_CONTEXT\n"
885            "AS_SUBSTITUTION\n"
886            'SUB GLYPH "a"\n'
887            'WITH GLYPH "a.alt"\n'
888            "END_SUB\n"
889            "END_SUBSTITUTION"
890        ).statements
891        self.assertEqual((lookup.name, lookup.reversal), ("Lookup", None))
892
893    def test_substitution_reversal(self):
894        lookup = self.parse(
895            'DEF_GROUP "DFLT_Num_standardFigures"\n'
896            ' ENUM GLYPH "zero" GLYPH "one" GLYPH "two" END_ENUM\n'
897            "END_GROUP\n"
898            'DEF_GROUP "DFLT_Num_numerators"\n'
899            ' ENUM GLYPH "zero.numr" GLYPH "one.numr" GLYPH "two.numr" END_ENUM\n'
900            "END_GROUP\n"
901            'DEF_LOOKUP "RevLookup" PROCESS_BASE PROCESS_MARKS ALL '
902            "DIRECTION LTR REVERSAL\n"
903            "IN_CONTEXT\n"
904            ' RIGHT ENUM GLYPH "a" GLYPH "b" END_ENUM\n'
905            "END_CONTEXT\n"
906            "AS_SUBSTITUTION\n"
907            'SUB GROUP "DFLT_Num_standardFigures"\n'
908            'WITH GROUP "DFLT_Num_numerators"\n'
909            "END_SUB\n"
910            "END_SUBSTITUTION"
911        ).statements[-1]
912        self.assertEqual((lookup.name, lookup.reversal), ("RevLookup", True))
913
914    def test_substitution_single_to_multiple(self):
915        [lookup] = self.parse(
916            'DEF_LOOKUP "ccmp" PROCESS_BASE PROCESS_MARKS ALL '
917            "DIRECTION LTR\n"
918            "IN_CONTEXT\n"
919            "END_CONTEXT\n"
920            "AS_SUBSTITUTION\n"
921            'SUB GLYPH "aacute"\n'
922            'WITH GLYPH "a" GLYPH "acutecomb"\n'
923            "END_SUB\n"
924            'SUB GLYPH "agrave"\n'
925            'WITH GLYPH "a" GLYPH "gravecomb"\n'
926            "END_SUB\n"
927            "END_SUBSTITUTION"
928        ).statements
929        self.assertEqual(lookup.name, "ccmp")
930        self.assertSubEqual(
931            lookup.sub,
932            [["aacute"], ["agrave"]],
933            [["a", "acutecomb"], ["a", "gravecomb"]],
934        )
935
936    def test_substitution_multiple_to_single(self):
937        [lookup] = self.parse(
938            'DEF_LOOKUP "liga" PROCESS_BASE PROCESS_MARKS ALL '
939            "DIRECTION LTR\n"
940            "IN_CONTEXT\n"
941            "END_CONTEXT\n"
942            "AS_SUBSTITUTION\n"
943            'SUB GLYPH "f" GLYPH "i"\n'
944            'WITH GLYPH "f_i"\n'
945            "END_SUB\n"
946            'SUB GLYPH "f" GLYPH "t"\n'
947            'WITH GLYPH "f_t"\n'
948            "END_SUB\n"
949            "END_SUBSTITUTION"
950        ).statements
951        self.assertEqual(lookup.name, "liga")
952        self.assertSubEqual(lookup.sub, [["f", "i"], ["f", "t"]], [["f_i"], ["f_t"]])
953
954    def test_substitution_reverse_chaining_single(self):
955        [lookup] = self.parse(
956            'DEF_LOOKUP "numr" PROCESS_BASE PROCESS_MARKS ALL '
957            "DIRECTION LTR REVERSAL\n"
958            "IN_CONTEXT\n"
959            " RIGHT ENUM "
960            'GLYPH "fraction" '
961            'RANGE "zero.numr" TO "nine.numr" '
962            "END_ENUM\n"
963            "END_CONTEXT\n"
964            "AS_SUBSTITUTION\n"
965            'SUB RANGE "zero" TO "nine"\n'
966            'WITH RANGE "zero.numr" TO "nine.numr"\n'
967            "END_SUB\n"
968            "END_SUBSTITUTION"
969        ).statements
970
971        mapping = lookup.sub.mapping
972        glyphs = [[(r.start, r.end) for r in v] for v in mapping.keys()]
973        replacement = [[(r.start, r.end) for r in v] for v in mapping.values()]
974
975        self.assertEqual(lookup.name, "numr")
976        self.assertEqual(glyphs, [[("zero", "nine")]])
977        self.assertEqual(replacement, [[("zero.numr", "nine.numr")]])
978
979        self.assertEqual(len(lookup.context[0].right), 1)
980        self.assertEqual(len(lookup.context[0].right[0]), 1)
981        enum = lookup.context[0].right[0][0]
982        self.assertEqual(len(enum.enum), 2)
983        self.assertEqual(enum.enum[0].glyph, "fraction")
984        self.assertEqual(
985            (enum.enum[1].start, enum.enum[1].end), ("zero.numr", "nine.numr")
986        )
987
988    # GPOS
989    #  ATTACH_CURSIVE
990    #  ATTACH
991    #  ADJUST_PAIR
992    #  ADJUST_SINGLE
993    def test_position_empty(self):
994        with self.assertRaisesRegex(
995            VoltLibError, "Expected ATTACH, ATTACH_CURSIVE, ADJUST_PAIR, ADJUST_SINGLE"
996        ):
997            [lookup] = self.parse(
998                'DEF_LOOKUP "empty_position" PROCESS_BASE PROCESS_MARKS ALL '
999                "DIRECTION LTR\n"
1000                "EXCEPT_CONTEXT\n"
1001                ' LEFT GLYPH "glyph"\n'
1002                "END_CONTEXT\n"
1003                "AS_POSITION\n"
1004                "END_POSITION"
1005            ).statements
1006
1007    def test_position_attach(self):
1008        [lookup, anchor1, anchor2, anchor3, anchor4] = self.parse(
1009            'DEF_LOOKUP "anchor_top" PROCESS_BASE PROCESS_MARKS ALL '
1010            "DIRECTION RTL\n"
1011            "IN_CONTEXT\n"
1012            "END_CONTEXT\n"
1013            "AS_POSITION\n"
1014            'ATTACH GLYPH "a" GLYPH "e"\n'
1015            'TO GLYPH "acutecomb" AT ANCHOR "top" '
1016            'GLYPH "gravecomb" AT ANCHOR "top"\n'
1017            "END_ATTACH\n"
1018            "END_POSITION\n"
1019            'DEF_ANCHOR "MARK_top" ON 120 GLYPH acutecomb COMPONENT 1 '
1020            "AT  POS DX 0 DY 450 END_POS END_ANCHOR\n"
1021            'DEF_ANCHOR "MARK_top" ON 121 GLYPH gravecomb COMPONENT 1 '
1022            "AT  POS DX 0 DY 450 END_POS END_ANCHOR\n"
1023            'DEF_ANCHOR "top" ON 31 GLYPH a COMPONENT 1 '
1024            "AT  POS DX 210 DY 450 END_POS END_ANCHOR\n"
1025            'DEF_ANCHOR "top" ON 35 GLYPH e COMPONENT 1 '
1026            "AT  POS DX 215 DY 450 END_POS END_ANCHOR"
1027        ).statements
1028        pos = lookup.pos
1029        coverage = [g.glyph for g in pos.coverage]
1030        coverage_to = [[[g.glyph for g in e], a] for (e, a) in pos.coverage_to]
1031        self.assertEqual(
1032            (lookup.name, coverage, coverage_to),
1033            (
1034                "anchor_top",
1035                ["a", "e"],
1036                [[["acutecomb"], "top"], [["gravecomb"], "top"]],
1037            ),
1038        )
1039        self.assertEqual(
1040            (
1041                anchor1.name,
1042                anchor1.gid,
1043                anchor1.glyph_name,
1044                anchor1.component,
1045                anchor1.locked,
1046                anchor1.pos,
1047            ),
1048            ("MARK_top", 120, "acutecomb", 1, False, (None, 0, 450, {}, {}, {})),
1049        )
1050        self.assertEqual(
1051            (
1052                anchor2.name,
1053                anchor2.gid,
1054                anchor2.glyph_name,
1055                anchor2.component,
1056                anchor2.locked,
1057                anchor2.pos,
1058            ),
1059            ("MARK_top", 121, "gravecomb", 1, False, (None, 0, 450, {}, {}, {})),
1060        )
1061        self.assertEqual(
1062            (
1063                anchor3.name,
1064                anchor3.gid,
1065                anchor3.glyph_name,
1066                anchor3.component,
1067                anchor3.locked,
1068                anchor3.pos,
1069            ),
1070            ("top", 31, "a", 1, False, (None, 210, 450, {}, {}, {})),
1071        )
1072        self.assertEqual(
1073            (
1074                anchor4.name,
1075                anchor4.gid,
1076                anchor4.glyph_name,
1077                anchor4.component,
1078                anchor4.locked,
1079                anchor4.pos,
1080            ),
1081            ("top", 35, "e", 1, False, (None, 215, 450, {}, {}, {})),
1082        )
1083
1084    def test_position_attach_cursive(self):
1085        [lookup] = self.parse(
1086            'DEF_LOOKUP "SomeLookup" PROCESS_BASE PROCESS_MARKS ALL '
1087            "DIRECTION RTL\n"
1088            "IN_CONTEXT\n"
1089            "END_CONTEXT\n"
1090            "AS_POSITION\n"
1091            'ATTACH_CURSIVE\nEXIT  GLYPH "a" GLYPH "b"\nENTER  GLYPH "c"\n'
1092            "END_ATTACH\n"
1093            "END_POSITION"
1094        ).statements
1095        exit = [[g.glyph for g in v] for v in lookup.pos.coverages_exit]
1096        enter = [[g.glyph for g in v] for v in lookup.pos.coverages_enter]
1097        self.assertEqual(
1098            (lookup.name, exit, enter), ("SomeLookup", [["a", "b"]], [["c"]])
1099        )
1100
1101    def test_position_adjust_pair(self):
1102        [lookup] = self.parse(
1103            'DEF_LOOKUP "kern1" PROCESS_BASE PROCESS_MARKS ALL '
1104            "DIRECTION RTL\n"
1105            "IN_CONTEXT\n"
1106            "END_CONTEXT\n"
1107            "AS_POSITION\n"
1108            "ADJUST_PAIR\n"
1109            ' FIRST  GLYPH "A"\n'
1110            ' SECOND  GLYPH "V"\n'
1111            " 1 2 BY POS ADV -30 END_POS POS END_POS\n"
1112            " 2 1 BY POS ADV -30 END_POS POS END_POS\n\n"
1113            "END_ADJUST\n"
1114            "END_POSITION"
1115        ).statements
1116        coverages_1 = [[g.glyph for g in v] for v in lookup.pos.coverages_1]
1117        coverages_2 = [[g.glyph for g in v] for v in lookup.pos.coverages_2]
1118        self.assertEqual(
1119            (lookup.name, coverages_1, coverages_2, lookup.pos.adjust_pair),
1120            (
1121                "kern1",
1122                [["A"]],
1123                [["V"]],
1124                {
1125                    (1, 2): (
1126                        (-30, None, None, {}, {}, {}),
1127                        (None, None, None, {}, {}, {}),
1128                    ),
1129                    (2, 1): (
1130                        (-30, None, None, {}, {}, {}),
1131                        (None, None, None, {}, {}, {}),
1132                    ),
1133                },
1134            ),
1135        )
1136
1137    def test_position_adjust_single(self):
1138        [lookup] = self.parse(
1139            'DEF_LOOKUP "TestLookup" PROCESS_BASE PROCESS_MARKS ALL '
1140            "DIRECTION LTR\n"
1141            "IN_CONTEXT\n"
1142            # ' LEFT GLYPH "leftGlyph"\n'
1143            # ' RIGHT GLYPH "rightGlyph"\n'
1144            "END_CONTEXT\n"
1145            "AS_POSITION\n"
1146            "ADJUST_SINGLE"
1147            ' GLYPH "glyph1" BY POS ADV 0 DX 123 END_POS'
1148            ' GLYPH "glyph2" BY POS ADV 0 DX 456 END_POS\n'
1149            "END_ADJUST\n"
1150            "END_POSITION"
1151        ).statements
1152        pos = lookup.pos
1153        adjust = [[[g.glyph for g in a], b] for (a, b) in pos.adjust_single]
1154        self.assertEqual(
1155            (lookup.name, adjust),
1156            (
1157                "TestLookup",
1158                [
1159                    [["glyph1"], (0, 123, None, {}, {}, {})],
1160                    [["glyph2"], (0, 456, None, {}, {}, {})],
1161                ],
1162            ),
1163        )
1164
1165    def test_def_anchor(self):
1166        [anchor1, anchor2, anchor3] = self.parse(
1167            'DEF_ANCHOR "top" ON 120 GLYPH a '
1168            "COMPONENT 1 AT  POS DX 250 DY 450 END_POS END_ANCHOR\n"
1169            'DEF_ANCHOR "MARK_top" ON 120 GLYPH acutecomb '
1170            "COMPONENT 1 AT  POS DX 0 DY 450 END_POS END_ANCHOR\n"
1171            'DEF_ANCHOR "bottom" ON 120 GLYPH a '
1172            "COMPONENT 1 AT  POS DX 250 DY 0 END_POS END_ANCHOR"
1173        ).statements
1174        self.assertEqual(
1175            (
1176                anchor1.name,
1177                anchor1.gid,
1178                anchor1.glyph_name,
1179                anchor1.component,
1180                anchor1.locked,
1181                anchor1.pos,
1182            ),
1183            ("top", 120, "a", 1, False, (None, 250, 450, {}, {}, {})),
1184        )
1185        self.assertEqual(
1186            (
1187                anchor2.name,
1188                anchor2.gid,
1189                anchor2.glyph_name,
1190                anchor2.component,
1191                anchor2.locked,
1192                anchor2.pos,
1193            ),
1194            ("MARK_top", 120, "acutecomb", 1, False, (None, 0, 450, {}, {}, {})),
1195        )
1196        self.assertEqual(
1197            (
1198                anchor3.name,
1199                anchor3.gid,
1200                anchor3.glyph_name,
1201                anchor3.component,
1202                anchor3.locked,
1203                anchor3.pos,
1204            ),
1205            ("bottom", 120, "a", 1, False, (None, 250, 0, {}, {}, {})),
1206        )
1207
1208    def test_def_anchor_multi_component(self):
1209        [anchor1, anchor2] = self.parse(
1210            'DEF_ANCHOR "top" ON 120 GLYPH a '
1211            "COMPONENT 1 AT  POS DX 250 DY 450 END_POS END_ANCHOR\n"
1212            'DEF_ANCHOR "top" ON 120 GLYPH a '
1213            "COMPONENT 2 AT  POS DX 250 DY 450 END_POS END_ANCHOR"
1214        ).statements
1215        self.assertEqual(
1216            (anchor1.name, anchor1.gid, anchor1.glyph_name, anchor1.component),
1217            ("top", 120, "a", 1),
1218        )
1219        self.assertEqual(
1220            (anchor2.name, anchor2.gid, anchor2.glyph_name, anchor2.component),
1221            ("top", 120, "a", 2),
1222        )
1223
1224    def test_def_anchor_duplicate(self):
1225        self.assertRaisesRegex(
1226            VoltLibError,
1227            'Anchor "dupe" already defined, ' "anchor names are case insensitive",
1228            self.parse,
1229            'DEF_ANCHOR "dupe" ON 120 GLYPH a '
1230            "COMPONENT 1 AT  POS DX 250 DY 450 END_POS END_ANCHOR\n"
1231            'DEF_ANCHOR "dupe" ON 120 GLYPH a '
1232            "COMPONENT 1 AT  POS DX 250 DY 450 END_POS END_ANCHOR",
1233        )
1234
1235    def test_def_anchor_locked(self):
1236        [anchor] = self.parse(
1237            'DEF_ANCHOR "top" ON 120 GLYPH a '
1238            "COMPONENT 1 LOCKED AT  POS DX 250 DY 450 END_POS END_ANCHOR"
1239        ).statements
1240        self.assertEqual(
1241            (
1242                anchor.name,
1243                anchor.gid,
1244                anchor.glyph_name,
1245                anchor.component,
1246                anchor.locked,
1247                anchor.pos,
1248            ),
1249            ("top", 120, "a", 1, True, (None, 250, 450, {}, {}, {})),
1250        )
1251
1252    def test_anchor_adjust_device(self):
1253        [anchor] = self.parse(
1254            'DEF_ANCHOR "MARK_top" ON 123 GLYPH diacglyph '
1255            "COMPONENT 1 AT  POS DX 0 DY 456 ADJUST_BY 12 AT 34 "
1256            "ADJUST_BY 56 AT 78 END_POS END_ANCHOR"
1257        ).statements
1258        self.assertEqual(
1259            (anchor.name, anchor.pos),
1260            ("MARK_top", (None, 0, 456, {}, {}, {34: 12, 78: 56})),
1261        )
1262
1263    def test_ppem(self):
1264        [grid_ppem, pres_ppem, ppos_ppem] = self.parse(
1265            "GRID_PPEM 20\n" "PRESENTATION_PPEM 72\n" "PPOSITIONING_PPEM 144"
1266        ).statements
1267        self.assertEqual(
1268            (
1269                (grid_ppem.name, grid_ppem.value),
1270                (pres_ppem.name, pres_ppem.value),
1271                (ppos_ppem.name, ppos_ppem.value),
1272            ),
1273            (("GRID_PPEM", 20), ("PRESENTATION_PPEM", 72), ("PPOSITIONING_PPEM", 144)),
1274        )
1275
1276    def test_compiler_flags(self):
1277        [setting1, setting2] = self.parse(
1278            "COMPILER_USEEXTENSIONLOOKUPS\n" "COMPILER_USEPAIRPOSFORMAT2"
1279        ).statements
1280        self.assertEqual(
1281            ((setting1.name, setting1.value), (setting2.name, setting2.value)),
1282            (
1283                ("COMPILER_USEEXTENSIONLOOKUPS", True),
1284                ("COMPILER_USEPAIRPOSFORMAT2", True),
1285            ),
1286        )
1287
1288    def test_cmap(self):
1289        [cmap_format1, cmap_format2, cmap_format3] = self.parse(
1290            "CMAP_FORMAT 0 3 4\n" "CMAP_FORMAT 1 0 6\n" "CMAP_FORMAT 3 1 4"
1291        ).statements
1292        self.assertEqual(
1293            (
1294                (cmap_format1.name, cmap_format1.value),
1295                (cmap_format2.name, cmap_format2.value),
1296                (cmap_format3.name, cmap_format3.value),
1297            ),
1298            (
1299                ("CMAP_FORMAT", (0, 3, 4)),
1300                ("CMAP_FORMAT", (1, 0, 6)),
1301                ("CMAP_FORMAT", (3, 1, 4)),
1302            ),
1303        )
1304
1305    def test_do_not_touch_cmap(self):
1306        [option1, option2, option3, option4] = self.parse(
1307            "DO_NOT_TOUCH_CMAP\n"
1308            "CMAP_FORMAT 0 3 4\n"
1309            "CMAP_FORMAT 1 0 6\n"
1310            "CMAP_FORMAT 3 1 4"
1311        ).statements
1312        self.assertEqual(
1313            (
1314                (option1.name, option1.value),
1315                (option2.name, option2.value),
1316                (option3.name, option3.value),
1317                (option4.name, option4.value),
1318            ),
1319            (
1320                ("DO_NOT_TOUCH_CMAP", True),
1321                ("CMAP_FORMAT", (0, 3, 4)),
1322                ("CMAP_FORMAT", (1, 0, 6)),
1323                ("CMAP_FORMAT", (3, 1, 4)),
1324            ),
1325        )
1326
1327    def test_stop_at_end(self):
1328        doc = self.parse_('DEF_GLYPH ".notdef" ID 0 TYPE BASE END_GLYPH END\0\0\0\0')
1329        [def_glyph] = doc.statements
1330        self.assertEqual(
1331            (
1332                def_glyph.name,
1333                def_glyph.id,
1334                def_glyph.unicode,
1335                def_glyph.type,
1336                def_glyph.components,
1337            ),
1338            (".notdef", 0, None, "BASE", None),
1339        )
1340        self.assertEqual(
1341            str(doc), '\nDEF_GLYPH ".notdef" ID 0 TYPE BASE END_GLYPH END\n'
1342        )
1343
1344    def parse_(self, text):
1345        return Parser(StringIO(text)).parse()
1346
1347    def parse(self, text):
1348        doc = self.parse_(text)
1349        self.assertEqual("\n".join(str(s) for s in doc.statements), text)
1350        return Parser(StringIO(text)).parse()
1351
1352
1353if __name__ == "__main__":
1354    import sys
1355
1356    sys.exit(unittest.main())
1357