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