Home | History | Annotate | Download | only in voltLib
      1 from __future__ import print_function, division, absolute_import
      2 from __future__ import unicode_literals
      3 from fontTools.voltLib import ast
      4 from fontTools.voltLib.error import VoltLibError
      5 from fontTools.voltLib.parser import Parser
      6 from io import open
      7 import os
      8 import shutil
      9 import tempfile
     10 import unittest
     11 
     12 
     13 class 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 
   1139 if __name__ == "__main__":
   1140     import sys
   1141     sys.exit(unittest.main())
   1142