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