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