Blob Blame History Raw
from __future__ import print_function, division, absolute_import
from __future__ import unicode_literals
from fontTools.voltLib.error import VoltLibError
from fontTools.voltLib.parser import Parser
from io import open
import os
import shutil
import tempfile
import unittest


class ParserTest(unittest.TestCase):
    def __init__(self, methodName):
        unittest.TestCase.__init__(self, methodName)
        # Python 3 renamed assertRaisesRegexp to assertRaisesRegex,
        # and fires deprecation warnings if a program uses the old name.
        if not hasattr(self, "assertRaisesRegex"):
            self.assertRaisesRegex = self.assertRaisesRegexp

    def test_def_glyph_base(self):
        [def_glyph] = self.parse(
            'DEF_GLYPH ".notdef" ID 0 TYPE BASE END_GLYPH'
        ).statements
        self.assertEqual((def_glyph.name, def_glyph.id, def_glyph.unicode,
                          def_glyph.type, def_glyph.components),
                         (".notdef", 0, None, "BASE", None))

    def test_def_glyph_base_with_unicode(self):
        [def_glyph] = self.parse(
            'DEF_GLYPH "space" ID 3 UNICODE 32 TYPE BASE END_GLYPH'
        ).statements
        self.assertEqual((def_glyph.name, def_glyph.id, def_glyph.unicode,
                          def_glyph.type, def_glyph.components),
                         ("space", 3, [0x0020], "BASE", None))

    def test_def_glyph_base_with_unicodevalues(self):
        [def_glyph] = self.parse(
            'DEF_GLYPH "CR" ID 2 UNICODEVALUES "U+0009" '
            'TYPE BASE END_GLYPH'
        ).statements
        self.assertEqual((def_glyph.name, def_glyph.id, def_glyph.unicode,
                          def_glyph.type, def_glyph.components),
                         ("CR", 2, [0x0009], "BASE", None))

    def test_def_glyph_base_with_mult_unicodevalues(self):
        [def_glyph] = self.parse(
            'DEF_GLYPH "CR" ID 2 UNICODEVALUES "U+0009,U+000D" '
            'TYPE BASE END_GLYPH'
        ).statements
        self.assertEqual((def_glyph.name, def_glyph.id, def_glyph.unicode,
                          def_glyph.type, def_glyph.components),
                         ("CR", 2, [0x0009, 0x000D], "BASE", None))

    def test_def_glyph_base_with_empty_unicodevalues(self):
        [def_glyph] = self.parse(
            'DEF_GLYPH "i.locl" ID 269 UNICODEVALUES "" '
            'TYPE BASE END_GLYPH'
        ).statements
        self.assertEqual((def_glyph.name, def_glyph.id, def_glyph.unicode,
                          def_glyph.type, def_glyph.components),
                         ("i.locl", 269, None, "BASE", None))

    def test_def_glyph_base_2_components(self):
        [def_glyph] = self.parse(
            'DEF_GLYPH "glyphBase" ID 320 TYPE BASE COMPONENTS 2 END_GLYPH'
        ).statements
        self.assertEqual((def_glyph.name, def_glyph.id, def_glyph.unicode,
                          def_glyph.type, def_glyph.components),
                         ("glyphBase", 320, None, "BASE", 2))

    def test_def_glyph_ligature_2_components(self):
        [def_glyph] = self.parse(
            'DEF_GLYPH "f_f" ID 320 TYPE LIGATURE COMPONENTS 2 END_GLYPH'
        ).statements
        self.assertEqual((def_glyph.name, def_glyph.id, def_glyph.unicode,
                          def_glyph.type, def_glyph.components),
                         ("f_f", 320, None, "LIGATURE", 2))

    def test_def_glyph_no_type(self):
        [def_glyph] = self.parse(
            'DEF_GLYPH "glyph20" ID 20 END_GLYPH'
        ).statements
        self.assertEqual((def_glyph.name, def_glyph.id, def_glyph.unicode,
                          def_glyph.type, def_glyph.components),
                         ("glyph20", 20, None, None, None))

    def test_def_glyph_case_sensitive(self):
        def_glyphs = self.parse(
            'DEF_GLYPH "A" ID 3 UNICODE 65 TYPE BASE END_GLYPH\n'
            'DEF_GLYPH "a" ID 4 UNICODE 97 TYPE BASE END_GLYPH\n'
        ).statements
        self.assertEqual((def_glyphs[0].name, def_glyphs[0].id,
                          def_glyphs[0].unicode, def_glyphs[0].type,
                          def_glyphs[0].components),
                         ("A", 3, [0x41], "BASE", None))
        self.assertEqual((def_glyphs[1].name, def_glyphs[1].id,
                          def_glyphs[1].unicode, def_glyphs[1].type,
                          def_glyphs[1].components),
                         ("a", 4, [0x61], "BASE", None))

    def test_def_group_glyphs(self):
        [def_group] = self.parse(
            'DEF_GROUP "aaccented"\n'
            'ENUM GLYPH "aacute" GLYPH "abreve" GLYPH "acircumflex" '
            'GLYPH "adieresis" GLYPH "ae" GLYPH "agrave" GLYPH "amacron" '
            'GLYPH "aogonek" GLYPH "aring" GLYPH "atilde" END_ENUM\n'
            'END_GROUP\n'
        ).statements
        self.assertEqual((def_group.name, def_group.enum),
                         ("aaccented",
                          ("aacute", "abreve", "acircumflex", "adieresis",
                           "ae", "agrave", "amacron", "aogonek", "aring",
                           "atilde")))

    def test_def_group_groups(self):
        [group1, group2, test_group] = self.parse(
            'DEF_GROUP "Group1"\n'
            'ENUM GLYPH "a" GLYPH "b" GLYPH "c" GLYPH "d" END_ENUM\n'
            'END_GROUP\n'
            'DEF_GROUP "Group2"\n'
            'ENUM GLYPH "e" GLYPH "f" GLYPH "g" GLYPH "h" END_ENUM\n'
            'END_GROUP\n'
            'DEF_GROUP "TestGroup"\n'
            'ENUM GROUP "Group1" GROUP "Group2" END_ENUM\n'
            'END_GROUP\n'
        ).statements
        self.assertEqual(
            (test_group.name, test_group.enum),
            ("TestGroup",
             (("Group1",), ("Group2",))))

    def test_def_group_groups_not_yet_defined(self):
        [group1, test_group1, test_group2, test_group3, group2] = self.parse(
            'DEF_GROUP "Group1"\n'
            'ENUM GLYPH "a" GLYPH "b" GLYPH "c" GLYPH "d" END_ENUM\n'
            'END_GROUP\n'
            'DEF_GROUP "TestGroup1"\n'
            'ENUM GROUP "Group1" GROUP "Group2" END_ENUM\n'
            'END_GROUP\n'
            'DEF_GROUP "TestGroup2"\n'
            'ENUM GROUP "Group2" END_ENUM\n'
            'END_GROUP\n'
            'DEF_GROUP "TestGroup3"\n'
            'ENUM GROUP "Group2" GROUP "Group1" END_ENUM\n'
            'END_GROUP\n'
            'DEF_GROUP "Group2"\n'
            'ENUM GLYPH "e" GLYPH "f" GLYPH "g" GLYPH "h" END_ENUM\n'
            'END_GROUP\n'
        ).statements
        self.assertEqual(
            (test_group1.name, test_group1.enum),
            ("TestGroup1",
             (("Group1", ), ("Group2", ))))
        self.assertEqual(
            (test_group2.name, test_group2.enum),
            ("TestGroup2",
             (("Group2", ), )))
        self.assertEqual(
            (test_group3.name, test_group3.enum),
            ("TestGroup3",
             (("Group2", ), ("Group1", ))))

    # def test_def_group_groups_undefined(self):
    #     with self.assertRaisesRegex(
    #             VoltLibError,
    #             r'Group "Group2" is used but undefined.'):
    #         [group1, test_group, group2] = self.parse(
    #             'DEF_GROUP "Group1"\n'
    #             'ENUM GLYPH "a" GLYPH "b" GLYPH "c" GLYPH "d" END_ENUM\n'
    #             'END_GROUP\n'
    #             'DEF_GROUP "TestGroup"\n'
    #             'ENUM GROUP "Group1" GROUP "Group2" END_ENUM\n'
    #             'END_GROUP\n'
    #         ).statements

    def test_def_group_glyphs_and_group(self):
        [def_group1, def_group2] = self.parse(
            'DEF_GROUP "aaccented"\n'
            'ENUM GLYPH "aacute" GLYPH "abreve" GLYPH "acircumflex" '
            'GLYPH "adieresis" GLYPH "ae" GLYPH "agrave" GLYPH "amacron" '
            'GLYPH "aogonek" GLYPH "aring" GLYPH "atilde" END_ENUM\n'
            'END_GROUP\n'
            'DEF_GROUP "KERN_lc_a_2ND"\n'
            'ENUM GLYPH "a" GROUP "aaccented" END_ENUM\n'
            'END_GROUP'
        ).statements
        self.assertEqual((def_group2.name, def_group2.enum),
                         ("KERN_lc_a_2ND",
                          ("a", ("aaccented", ))))

    def test_def_group_range(self):
        [def_group] = self.parse(
            'DEF_GROUP "KERN_lc_a_2ND"\n'
            'ENUM RANGE "a" TO "atilde" GLYPH "b" RANGE "c" TO "cdotaccent" '
            'END_ENUM\n'
            'END_GROUP'
        ).statements
        self.assertEqual((def_group.name, def_group.enum),
                         ("KERN_lc_a_2ND",
                          (("a", "atilde"), "b", ("c", "cdotaccent"))))

    def test_group_duplicate(self):
        self.assertRaisesRegex(
            VoltLibError,
            'Glyph group "dupe" already defined, '
            'group names are case insensitive',
            self.parse, 'DEF_GROUP "dupe"\n'
                        'ENUM GLYPH "a" GLYPH "b" END_ENUM\n'
                        'END_GROUP\n'
                        'DEF_GROUP "dupe"\n'
                        'ENUM GLYPH "x" END_ENUM\n'
                        'END_GROUP\n'
        )

    def test_group_duplicate_case_insensitive(self):
        self.assertRaisesRegex(
            VoltLibError,
            'Glyph group "Dupe" already defined, '
            'group names are case insensitive',
            self.parse, 'DEF_GROUP "dupe"\n'
                        'ENUM GLYPH "a" GLYPH "b" END_ENUM\n'
                        'END_GROUP\n'
                        'DEF_GROUP "Dupe"\n'
                        'ENUM GLYPH "x" END_ENUM\n'
                        'END_GROUP\n'
        )

    def test_script_without_langsys(self):
        [script] = self.parse(
            'DEF_SCRIPT NAME "Latin" TAG "latn"\n'
            'END_SCRIPT'
        ).statements
        self.assertEqual((script.name, script.tag, script.langs),
                         ("Latin", "latn", []))

    def test_langsys_normal(self):
        [def_script] = self.parse(
            'DEF_SCRIPT NAME "Latin" TAG "latn"\n'
            'DEF_LANGSYS NAME "Romanian" TAG "ROM "\n'
            'END_LANGSYS\n'
            'DEF_LANGSYS NAME "Moldavian" TAG "MOL "\n'
            'END_LANGSYS\n'
            'END_SCRIPT'
        ).statements
        self.assertEqual((def_script.name, def_script.tag),
                         ("Latin",
                          "latn"))
        def_lang = def_script.langs[0]
        self.assertEqual((def_lang.name, def_lang.tag),
                         ("Romanian",
                          "ROM "))
        def_lang = def_script.langs[1]
        self.assertEqual((def_lang.name, def_lang.tag),
                         ("Moldavian",
                          "MOL "))

    def test_langsys_no_script_name(self):
        [langsys] = self.parse(
            'DEF_SCRIPT TAG "latn"\n'
            'DEF_LANGSYS NAME "Default" TAG "dflt"\n'
            'END_LANGSYS\n'
            'END_SCRIPT'
        ).statements
        self.assertEqual((langsys.name, langsys.tag),
                         (None,
                          "latn"))
        lang = langsys.langs[0]
        self.assertEqual((lang.name, lang.tag),
                         ("Default",
                          "dflt"))

    def test_langsys_no_script_tag_fails(self):
        with self.assertRaisesRegex(
                VoltLibError,
                r'.*Expected "TAG"'):
            [langsys] = self.parse(
                'DEF_SCRIPT NAME "Latin"\n'
                'DEF_LANGSYS NAME "Default" TAG "dflt"\n'
                'END_LANGSYS\n'
                'END_SCRIPT'
            ).statements

    def test_langsys_duplicate_script(self):
        with self.assertRaisesRegex(
                VoltLibError,
                'Script "DFLT" already defined, '
                'script tags are case insensitive'):
            [langsys1, langsys2] = self.parse(
                'DEF_SCRIPT NAME "Default" TAG "DFLT"\n'
                'DEF_LANGSYS NAME "Default" TAG "dflt"\n'
                'END_LANGSYS\n'
                'END_SCRIPT\n'
                'DEF_SCRIPT TAG "DFLT"\n'
                'DEF_LANGSYS NAME "Default" TAG "dflt"\n'
                'END_LANGSYS\n'
                'END_SCRIPT'
            ).statements

    def test_langsys_duplicate_lang(self):
        with self.assertRaisesRegex(
                VoltLibError,
                'Language "dflt" already defined in script "DFLT", '
                'language tags are case insensitive'):
            [langsys] = self.parse(
                'DEF_SCRIPT NAME "Default" TAG "DFLT"\n'
                'DEF_LANGSYS NAME "Default" TAG "dflt"\n'
                'END_LANGSYS\n'
                'DEF_LANGSYS NAME "Default" TAG "dflt"\n'
                'END_LANGSYS\n'
                'END_SCRIPT\n'
            ).statements

    def test_langsys_lang_in_separate_scripts(self):
        [langsys1, langsys2] = self.parse(
            'DEF_SCRIPT NAME "Default" TAG "DFLT"\n'
            'DEF_LANGSYS NAME "Default" TAG "dflt"\n'
            'END_LANGSYS\n'
            'DEF_LANGSYS NAME "Default" TAG "ROM "\n'
            'END_LANGSYS\n'
            'END_SCRIPT\n'
            'DEF_SCRIPT NAME "Latin" TAG "latn"\n'
            'DEF_LANGSYS NAME "Default" TAG "dflt"\n'
            'END_LANGSYS\n'
            'DEF_LANGSYS NAME "Default" TAG "ROM "\n'
            'END_LANGSYS\n'
            'END_SCRIPT'
        ).statements
        self.assertEqual((langsys1.langs[0].tag, langsys1.langs[1].tag),
                         ("dflt", "ROM "))
        self.assertEqual((langsys2.langs[0].tag, langsys2.langs[1].tag),
                         ("dflt", "ROM "))

    def test_langsys_no_lang_name(self):
        [langsys] = self.parse(
            'DEF_SCRIPT NAME "Latin" TAG "latn"\n'
            'DEF_LANGSYS TAG "dflt"\n'
            'END_LANGSYS\n'
            'END_SCRIPT'
        ).statements
        self.assertEqual((langsys.name, langsys.tag),
                         ("Latin",
                          "latn"))
        lang = langsys.langs[0]
        self.assertEqual((lang.name, lang.tag),
                         (None,
                          "dflt"))

    def test_langsys_no_langsys_tag_fails(self):
        with self.assertRaisesRegex(
                VoltLibError,
                r'.*Expected "TAG"'):
            [langsys] = self.parse(
                'DEF_SCRIPT NAME "Latin" TAG "latn"\n'
                'DEF_LANGSYS NAME "Default"\n'
                'END_LANGSYS\n'
                'END_SCRIPT'
            ).statements

    def test_feature(self):
        [def_script] = self.parse(
            'DEF_SCRIPT NAME "Latin" TAG "latn"\n'
            'DEF_LANGSYS NAME "Romanian" TAG "ROM "\n'
            'DEF_FEATURE NAME "Fractions" TAG "frac"\n'
            'LOOKUP "fraclookup"\n'
            'END_FEATURE\n'
            'END_LANGSYS\n'
            'END_SCRIPT'
        ).statements
        def_feature = def_script.langs[0].features[0]
        self.assertEqual((def_feature.name, def_feature.tag,
                          def_feature.lookups),
                         ("Fractions",
                          "frac",
                          ["fraclookup"]))
        [def_script] = self.parse(
            'DEF_SCRIPT NAME "Latin" TAG "latn"\n'
            'DEF_LANGSYS NAME "Romanian" TAG "ROM "\n'
            'DEF_FEATURE NAME "Kerning" TAG "kern"\n'
            'LOOKUP "kern1" LOOKUP "kern2"\n'
            'END_FEATURE\n'
            'END_LANGSYS\n'
            'END_SCRIPT'
        ).statements
        def_feature = def_script.langs[0].features[0]
        self.assertEqual((def_feature.name, def_feature.tag,
                          def_feature.lookups),
                         ("Kerning",
                          "kern",
                          ["kern1", "kern2"]))

    def test_lookup_duplicate(self):
        with self.assertRaisesRegex(
            VoltLibError,
            'Lookup "dupe" already defined, '
            'lookup names are case insensitive',
        ):
            [lookup1, lookup2] = self.parse(
                'DEF_LOOKUP "dupe"\n'
                'AS_SUBSTITUTION\n'
                'SUB GLYPH "a"\n'
                'WITH GLYPH "a.alt"\n'
                'END_SUB\n'
                'END_SUBSTITUTION\n'
                'DEF_LOOKUP "dupe"\n'
                'AS_SUBSTITUTION\n'
                'SUB GLYPH "b"\n'
                'WITH GLYPH "b.alt"\n'
                'END_SUB\n'
                'END_SUBSTITUTION\n'
            ).statements

    def test_lookup_duplicate_insensitive_case(self):
        with self.assertRaisesRegex(
            VoltLibError,
            'Lookup "Dupe" already defined, '
            'lookup names are case insensitive',
        ):
            [lookup1, lookup2] = self.parse(
                'DEF_LOOKUP "dupe"\n'
                'AS_SUBSTITUTION\n'
                'SUB GLYPH "a"\n'
                'WITH GLYPH "a.alt"\n'
                'END_SUB\n'
                'END_SUBSTITUTION\n'
                'DEF_LOOKUP "Dupe"\n'
                'AS_SUBSTITUTION\n'
                'SUB GLYPH "b"\n'
                'WITH GLYPH "b.alt"\n'
                'END_SUB\n'
                'END_SUBSTITUTION\n'
            ).statements

    def test_lookup_name_starts_with_letter(self):
        with self.assertRaisesRegex(
            VoltLibError,
            'Lookup name "\\\lookupname" must start with a letter'
        ):
            [lookup] = self.parse(
                'DEF_LOOKUP "\lookupname"\n'
                'AS_SUBSTITUTION\n'
                'SUB GLYPH "a"\n'
                'WITH GLYPH "a.alt"\n'
                'END_SUB\n'
                'END_SUBSTITUTION\n'
            ).statements

    def test_substitution_empty(self):
        with self.assertRaisesRegex(
                VoltLibError,
                r'Expected SUB'):
            [lookup] = self.parse(
                'DEF_LOOKUP "empty_substitution" PROCESS_BASE PROCESS_MARKS '
                'ALL DIRECTION LTR\n'
                'IN_CONTEXT\n'
                'END_CONTEXT\n'
                'AS_SUBSTITUTION\n'
                'END_SUBSTITUTION'
            ).statements

    def test_substitution_invalid_many_to_many(self):
        with self.assertRaisesRegex(
                VoltLibError,
                r'Invalid substitution type'):
            [lookup] = self.parse(
                'DEF_LOOKUP "invalid_substitution" PROCESS_BASE PROCESS_MARKS '
                'ALL DIRECTION LTR\n'
                'IN_CONTEXT\n'
                'END_CONTEXT\n'
                'AS_SUBSTITUTION\n'
                'SUB GLYPH "f" GLYPH "i"\n'
                'WITH GLYPH "f.alt" GLYPH "i.alt"\n'
                'END_SUB\n'
                'END_SUBSTITUTION'
            ).statements

    def test_substitution_invalid_reverse_chaining_single(self):
        with self.assertRaisesRegex(
                VoltLibError,
                r'Invalid substitution type'):
            [lookup] = self.parse(
                'DEF_LOOKUP "invalid_substitution" PROCESS_BASE PROCESS_MARKS '
                'ALL DIRECTION LTR REVERSAL\n'
                'IN_CONTEXT\n'
                'END_CONTEXT\n'
                'AS_SUBSTITUTION\n'
                'SUB GLYPH "f" GLYPH "i"\n'
                'WITH GLYPH "f_i"\n'
                'END_SUB\n'
                'END_SUBSTITUTION'
            ).statements

    def test_substitution_invalid_mixed(self):
        with self.assertRaisesRegex(
                VoltLibError,
                r'Invalid substitution type'):
            [lookup] = self.parse(
                'DEF_LOOKUP "invalid_substitution" PROCESS_BASE PROCESS_MARKS '
                'ALL DIRECTION LTR\n'
                'IN_CONTEXT\n'
                'END_CONTEXT\n'
                'AS_SUBSTITUTION\n'
                'SUB GLYPH "fi"\n'
                'WITH GLYPH "f" GLYPH "i"\n'
                'END_SUB\n'
                'SUB GLYPH "f" GLYPH "l"\n'
                'WITH GLYPH "f_l"\n'
                'END_SUB\n'
                'END_SUBSTITUTION'
            ).statements

    def test_substitution_single(self):
        [lookup] = self.parse(
            'DEF_LOOKUP "smcp" PROCESS_BASE PROCESS_MARKS ALL '
            'DIRECTION LTR\n'
            'IN_CONTEXT\n'
            'END_CONTEXT\n'
            'AS_SUBSTITUTION\n'
            'SUB GLYPH "a"\n'
            'WITH GLYPH "a.sc"\n'
            'END_SUB\n'
            'SUB GLYPH "b"\n'
            'WITH GLYPH "b.sc"\n'
            'END_SUB\n'
            'END_SUBSTITUTION'
        ).statements
        self.assertEqual((lookup.name, list(lookup.sub.mapping.items())),
                         ("smcp", [(("a",), ("a.sc",)), (("b",), ("b.sc",))]))

    def test_substitution_single_in_context(self):
        [group, lookup] = self.parse(
            'DEF_GROUP "Denominators" ENUM GLYPH "one.dnom" GLYPH "two.dnom" '
            'END_ENUM END_GROUP\n'
            'DEF_LOOKUP "fracdnom" PROCESS_BASE PROCESS_MARKS ALL '
            'DIRECTION LTR\n'
            'IN_CONTEXT LEFT ENUM GROUP "Denominators" GLYPH "fraction" '
            'END_ENUM\n'
            'END_CONTEXT\n'
            'AS_SUBSTITUTION\n'
            'SUB GLYPH "one"\n'
            'WITH GLYPH "one.dnom"\n'
            'END_SUB\n'
            'SUB GLYPH "two"\n'
            'WITH GLYPH "two.dnom"\n'
            'END_SUB\n'
            'END_SUBSTITUTION'
        ).statements
        context = lookup.context[0]
        self.assertEqual(
            (lookup.name, list(lookup.sub.mapping.items()),
             context.ex_or_in, context.left, context.right),
            ("fracdnom", [(("one",), ("one.dnom",)), (("two",), ("two.dnom",))],
             "IN_CONTEXT", [((("Denominators",), "fraction"),)], [])
        )

    def test_substitution_single_in_contexts(self):
        [group, lookup] = self.parse(
            'DEF_GROUP "Hebrew" ENUM GLYPH "uni05D0" GLYPH "uni05D1" '
            'END_ENUM END_GROUP\n'
            'DEF_LOOKUP "HebrewCurrency" PROCESS_BASE PROCESS_MARKS ALL '
            'DIRECTION LTR\n'
            'IN_CONTEXT\n'
            'RIGHT GROUP "Hebrew"\n'
            'RIGHT GLYPH "one.Hebr"\n'
            'END_CONTEXT\n'
            'IN_CONTEXT\n'
            'LEFT GROUP "Hebrew"\n'
            'LEFT GLYPH "one.Hebr"\n'
            'END_CONTEXT\n'
            'AS_SUBSTITUTION\n'
            'SUB GLYPH "dollar"\n'
            'WITH GLYPH "dollar.Hebr"\n'
            'END_SUB\n'
            'END_SUBSTITUTION'
        ).statements
        context1 = lookup.context[0]
        context2 = lookup.context[1]
        self.assertEqual(
            (lookup.name, context1.ex_or_in, context1.left,
             context1.right, context2.ex_or_in,
             context2.left, context2.right),
            ("HebrewCurrency", "IN_CONTEXT", [],
             [(("Hebrew",),), ("one.Hebr",)], "IN_CONTEXT",
             [(("Hebrew",),), ("one.Hebr",)], []))

    def test_substitution_skip_base(self):
        [group, lookup] = self.parse(
            'DEF_GROUP "SomeMarks" ENUM GLYPH "marka" GLYPH "markb" '
            'END_ENUM END_GROUP\n'
            'DEF_LOOKUP "SomeSub" SKIP_BASE PROCESS_MARKS ALL '
            'DIRECTION LTR\n'
            'IN_CONTEXT\n'
            'END_CONTEXT\n'
            'AS_SUBSTITUTION\n'
            'SUB GLYPH "A"\n'
            'WITH GLYPH "A.c2sc"\n'
            'END_SUB\n'
            'END_SUBSTITUTION'
        ).statements
        process_base = lookup.process_base
        self.assertEqual(
            (lookup.name, process_base),
            ("SomeSub", False))

    def test_substitution_process_base(self):
        [group, lookup] = self.parse(
            'DEF_GROUP "SomeMarks" ENUM GLYPH "marka" GLYPH "markb" '
            'END_ENUM END_GROUP\n'
            'DEF_LOOKUP "SomeSub" PROCESS_BASE PROCESS_MARKS ALL '
            'DIRECTION LTR\n'
            'IN_CONTEXT\n'
            'END_CONTEXT\n'
            'AS_SUBSTITUTION\n'
            'SUB GLYPH "A"\n'
            'WITH GLYPH "A.c2sc"\n'
            'END_SUB\n'
            'END_SUBSTITUTION'
        ).statements
        process_base = lookup.process_base
        self.assertEqual(
            (lookup.name, process_base),
            ("SomeSub", True))

    def test_substitution_skip_marks(self):
        [group, lookup] = self.parse(
            'DEF_GROUP "SomeMarks" ENUM GLYPH "marka" GLYPH "markb" '
            'END_ENUM END_GROUP\n'
            'DEF_LOOKUP "SomeSub" PROCESS_BASE SKIP_MARKS '
            'DIRECTION LTR\n'
            'IN_CONTEXT\n'
            'END_CONTEXT\n'
            'AS_SUBSTITUTION\n'
            'SUB GLYPH "A"\n'
            'WITH GLYPH "A.c2sc"\n'
            'END_SUB\n'
            'END_SUBSTITUTION'
        ).statements
        process_marks = lookup.process_marks
        self.assertEqual(
            (lookup.name, process_marks),
            ("SomeSub", False))

    def test_substitution_process_marks(self):
        [group, lookup] = self.parse(
            'DEF_GROUP "SomeMarks" ENUM GLYPH "acutecmb" GLYPH "gravecmb" '
            'END_ENUM END_GROUP\n'
            'DEF_LOOKUP "SomeSub" PROCESS_BASE '
            'PROCESS_MARKS "SomeMarks" \n'
            'DIRECTION RTL\n'
            'AS_SUBSTITUTION\n'
            'SUB GLYPH "A"\n'
            'WITH GLYPH "A.c2sc"\n'
            'END_SUB\n'
            'END_SUBSTITUTION'
        ).statements
        process_marks = lookup.process_marks
        self.assertEqual(
            (lookup.name, process_marks),
            ("SomeSub", "SomeMarks"))

    def test_substitution_process_all_marks(self):
        [group, lookup] = self.parse(
            'DEF_GROUP "SomeMarks" ENUM GLYPH "acutecmb" GLYPH "gravecmb" '
            'END_ENUM END_GROUP\n'
            'DEF_LOOKUP "SomeSub" PROCESS_BASE '
            'PROCESS_MARKS ALL \n'
            'DIRECTION RTL\n'
            'AS_SUBSTITUTION\n'
            'SUB GLYPH "A"\n'
            'WITH GLYPH "A.c2sc"\n'
            'END_SUB\n'
            'END_SUBSTITUTION'
        ).statements
        process_marks = lookup.process_marks
        self.assertEqual(
            (lookup.name, process_marks),
            ("SomeSub", True))

    def test_substitution_no_reversal(self):
        # TODO: check right context with no reversal
        [lookup] = self.parse(
            'DEF_LOOKUP "Lookup" PROCESS_BASE PROCESS_MARKS ALL '
            'DIRECTION LTR\n'
            'IN_CONTEXT\n'
            'RIGHT ENUM GLYPH "a" GLYPH "b" END_ENUM\n'
            'END_CONTEXT\n'
            'AS_SUBSTITUTION\n'
            'SUB GLYPH "a"\n'
            'WITH GLYPH "a.alt"\n'
            'END_SUB\n'
            'END_SUBSTITUTION'
        ).statements
        self.assertEqual(
            (lookup.name, lookup.reversal),
            ("Lookup", None)
        )

    def test_substitution_reversal(self):
        [lookup] = self.parse(
            'DEF_LOOKUP "RevLookup" PROCESS_BASE PROCESS_MARKS ALL '
            'DIRECTION LTR REVERSAL\n'
            'IN_CONTEXT\n'
            'RIGHT ENUM GLYPH "a" GLYPH "b" END_ENUM\n'
            'END_CONTEXT\n'
            'AS_SUBSTITUTION\n'
            'SUB GROUP "DFLT_Num_standardFigures"\n'
            'WITH GROUP "DFLT_Num_numerators"\n'
            'END_SUB\n'
            'END_SUBSTITUTION'
        ).statements
        self.assertEqual(
            (lookup.name, lookup.reversal),
            ("RevLookup", True)
        )

    def test_substitution_single_to_multiple(self):
        [lookup] = self.parse(
            'DEF_LOOKUP "ccmp" PROCESS_BASE PROCESS_MARKS ALL '
            'DIRECTION LTR\n'
            'IN_CONTEXT\n'
            'END_CONTEXT\n'
            'AS_SUBSTITUTION\n'
            'SUB GLYPH "aacute"\n'
            'WITH GLYPH "a" GLYPH "acutecomb"\n'
            'END_SUB\n'
            'SUB GLYPH "agrave"\n'
            'WITH GLYPH "a" GLYPH "gravecomb"\n'
            'END_SUB\n'
            'END_SUBSTITUTION'
        ).statements
        self.assertEqual((lookup.name, list(lookup.sub.mapping.items())),
                         ("ccmp",
                          [(("aacute",), ("a", "acutecomb")),
                           (("agrave",), ("a", "gravecomb"))]
                          ))

    def test_substitution_multiple_to_single(self):
        [lookup] = self.parse(
            'DEF_LOOKUP "liga" PROCESS_BASE PROCESS_MARKS ALL '
            'DIRECTION LTR\n'
            'IN_CONTEXT\n'
            'END_CONTEXT\n'
            'AS_SUBSTITUTION\n'
            'SUB GLYPH "f" GLYPH "i"\n'
            'WITH GLYPH "f_i"\n'
            'END_SUB\n'
            'SUB GLYPH "f" GLYPH "t"\n'
            'WITH GLYPH "f_t"\n'
            'END_SUB\n'
            'END_SUBSTITUTION'
        ).statements
        self.assertEqual((lookup.name, list(lookup.sub.mapping.items())),
                         ("liga",
                          [(("f", "i"), ("f_i",)),
                           (("f", "t"), ("f_t",))]))

    def test_substitution_reverse_chaining_single(self):
        [lookup] = self.parse(
            'DEF_LOOKUP "numr" PROCESS_BASE PROCESS_MARKS ALL '
            'DIRECTION LTR REVERSAL\n'
            'IN_CONTEXT\n'
            'RIGHT ENUM '
            'GLYPH "fraction" '
            'RANGE "zero.numr" TO "nine.numr" '
            'END_ENUM\n'
            'END_CONTEXT\n'
            'AS_SUBSTITUTION\n'
            'SUB RANGE "zero" TO "nine"\n'
            'WITH RANGE "zero.numr" TO "nine.numr"\n'
            'END_SUB\n'
            'END_SUBSTITUTION'
        ).statements
        self.assertEqual(
            (lookup.name, lookup.context[0].right,
             list(lookup.sub.mapping.items())),
            ("numr", [(("fraction", ("zero.numr", "nine.numr")),)],
             [((("zero", "nine"),), (("zero.numr", "nine.numr"),))]))

    # GPOS
    #  ATTACH_CURSIVE
    #  ATTACH
    #  ADJUST_PAIR
    #  ADJUST_SINGLE
    def test_position_empty(self):
        with self.assertRaisesRegex(
                VoltLibError,
                'Expected ATTACH, ATTACH_CURSIVE, ADJUST_PAIR, ADJUST_SINGLE'):
            [lookup] = self.parse(
                'DEF_LOOKUP "empty_position" PROCESS_BASE PROCESS_MARKS ALL '
                'DIRECTION LTR\n'
                'EXCEPT_CONTEXT\n'
                'LEFT GLYPH "glyph"\n'
                'END_CONTEXT\n'
                'AS_POSITION\n'
                'END_POSITION'
            ).statements

    def test_position_attach(self):
        [lookup, anchor1, anchor2, anchor3, anchor4] = self.parse(
            'DEF_LOOKUP "anchor_top" PROCESS_BASE PROCESS_MARKS ALL '
            'DIRECTION RTL\n'
            'IN_CONTEXT\n'
            'END_CONTEXT\n'
            'AS_POSITION\n'
            'ATTACH GLYPH "a" GLYPH "e"\n'
            'TO GLYPH "acutecomb" AT ANCHOR "top" '
            'GLYPH "gravecomb" AT ANCHOR "top"\n'
            'END_ATTACH\n'
            'END_POSITION\n'
            'DEF_ANCHOR "MARK_top" ON 120 GLYPH acutecomb COMPONENT 1 '
            'AT POS DX 0 DY 450 END_POS END_ANCHOR\n'
            'DEF_ANCHOR "MARK_top" ON 121 GLYPH gravecomb COMPONENT 1 '
            'AT POS DX 0 DY 450 END_POS END_ANCHOR\n'
            'DEF_ANCHOR "top" ON 31 GLYPH a COMPONENT 1 '
            'AT POS DX 210 DY 450 END_POS END_ANCHOR\n'
            'DEF_ANCHOR "top" ON 35 GLYPH e COMPONENT 1 '
            'AT POS DX 215 DY 450 END_POS END_ANCHOR\n'
        ).statements
        self.assertEqual(
            (lookup.name, lookup.pos.coverage, lookup.pos.coverage_to),
            ("anchor_top", ("a", "e"), [(("acutecomb",), "top"),
                                        (("gravecomb",), "top")])
        )
        self.assertEqual(
            (anchor1.name, anchor1.gid, anchor1.glyph_name, anchor1.component,
             anchor1.locked, anchor1.pos),
            ("MARK_top", 120, "acutecomb", 1, False, (None, 0, 450, {}, {},
             {}))
        )
        self.assertEqual(
            (anchor2.name, anchor2.gid, anchor2.glyph_name, anchor2.component,
             anchor2.locked, anchor2.pos),
            ("MARK_top", 121, "gravecomb", 1, False, (None, 0, 450, {}, {},
             {}))
        )
        self.assertEqual(
            (anchor3.name, anchor3.gid, anchor3.glyph_name, anchor3.component,
             anchor3.locked, anchor3.pos),
            ("top", 31, "a", 1, False, (None, 210, 450, {}, {}, {}))
        )
        self.assertEqual(
            (anchor4.name, anchor4.gid, anchor4.glyph_name, anchor4.component,
             anchor4.locked, anchor4.pos),
            ("top", 35, "e", 1, False, (None, 215, 450, {}, {}, {}))
        )

    def test_position_attach_cursive(self):
        [lookup] = self.parse(
            'DEF_LOOKUP "SomeLookup" PROCESS_BASE PROCESS_MARKS ALL '
            'DIRECTION RTL\n'
            'IN_CONTEXT\n'
            'END_CONTEXT\n'
            'AS_POSITION\n'
            'ATTACH_CURSIVE EXIT GLYPH "a" GLYPH "b" ENTER GLYPH "c"\n'
            'END_ATTACH\n'
            'END_POSITION\n'
        ).statements
        self.assertEqual(
            (lookup.name,
             lookup.pos.coverages_exit, lookup.pos.coverages_enter),
            ("SomeLookup",
             [("a", "b")], [("c",)])
        )

    def test_position_adjust_pair(self):
        [lookup] = self.parse(
            'DEF_LOOKUP "kern1" PROCESS_BASE PROCESS_MARKS ALL '
            'DIRECTION RTL\n'
            'IN_CONTEXT\n'
            'END_CONTEXT\n'
            'AS_POSITION\n'
            'ADJUST_PAIR\n'
            ' FIRST GLYPH "A"\n'
            ' SECOND GLYPH "V"\n'
            ' 1 2 BY POS ADV -30 END_POS POS END_POS\n'
            ' 2 1 BY POS ADV -30 END_POS POS END_POS\n'
            'END_ADJUST\n'
            'END_POSITION\n'
        ).statements
        self.assertEqual(
            (lookup.name, lookup.pos.coverages_1, lookup.pos.coverages_2,
             lookup.pos.adjust_pair),
            ("kern1", [("A",)], [("V",)],
             {(1, 2): ((-30, None, None, {}, {}, {}),
                       (None, None, None, {}, {}, {})),
              (2, 1): ((-30, None, None, {}, {}, {}),
                       (None, None, None, {}, {}, {}))})
        )

    def test_position_adjust_single(self):
        [lookup] = self.parse(
            'DEF_LOOKUP "TestLookup" PROCESS_BASE PROCESS_MARKS ALL '
            'DIRECTION LTR\n'
            'IN_CONTEXT\n'
            # 'LEFT GLYPH "leftGlyph"\n'
            # 'RIGHT GLYPH "rightGlyph"\n'
            'END_CONTEXT\n'
            'AS_POSITION\n'
            'ADJUST_SINGLE'
            ' GLYPH "glyph1" BY POS ADV 0 DX 123 END_POS\n'
            ' GLYPH "glyph2" BY POS ADV 0 DX 456 END_POS\n'
            'END_ADJUST\n'
            'END_POSITION\n'
        ).statements
        self.assertEqual(
            (lookup.name, lookup.pos.adjust_single),
            ("TestLookup",
             [(("glyph1",), (0, 123, None, {}, {}, {})),
              (("glyph2",), (0, 456, None, {}, {}, {}))])
        )

    def test_def_anchor(self):
        [anchor1, anchor2, anchor3] = self.parse(
            'DEF_ANCHOR "top" ON 120 GLYPH a '
            'COMPONENT 1 AT POS DX 250 DY 450 END_POS END_ANCHOR\n'
            'DEF_ANCHOR "MARK_top" ON 120 GLYPH acutecomb '
            'COMPONENT 1 AT POS DX 0 DY 450 END_POS END_ANCHOR\n'
            'DEF_ANCHOR "bottom" ON 120 GLYPH a '
            'COMPONENT 1 AT POS DX 250 DY 0 END_POS END_ANCHOR\n'
        ).statements
        self.assertEqual(
            (anchor1.name, anchor1.gid, anchor1.glyph_name, anchor1.component,
             anchor1.locked, anchor1.pos),
            ("top", 120, "a", 1,
             False, (None, 250, 450, {}, {}, {}))
        )
        self.assertEqual(
            (anchor2.name, anchor2.gid, anchor2.glyph_name, anchor2.component,
             anchor2.locked, anchor2.pos),
            ("MARK_top", 120, "acutecomb", 1,
             False, (None, 0, 450, {}, {}, {}))
        )
        self.assertEqual(
            (anchor3.name, anchor3.gid, anchor3.glyph_name, anchor3.component,
             anchor3.locked, anchor3.pos),
            ("bottom", 120, "a", 1,
             False, (None, 250, 0, {}, {}, {}))
        )

    def test_def_anchor_duplicate(self):
        self.assertRaisesRegex(
            VoltLibError,
            'Anchor "dupe" already defined, '
            'anchor names are case insensitive',
            self.parse,
            'DEF_ANCHOR "dupe" ON 120 GLYPH a '
            'COMPONENT 1 AT POS DX 250 DY 450 END_POS END_ANCHOR\n'
            'DEF_ANCHOR "dupe" ON 120 GLYPH a '
            'COMPONENT 1 AT POS DX 250 DY 450 END_POS END_ANCHOR\n'
        )

    def test_def_anchor_locked(self):
        [anchor] = self.parse(
            'DEF_ANCHOR "top" ON 120 GLYPH a '
            'COMPONENT 1 LOCKED AT POS DX 250 DY 450 END_POS END_ANCHOR\n'
        ).statements
        self.assertEqual(
            (anchor.name, anchor.gid, anchor.glyph_name, anchor.component,
             anchor.locked, anchor.pos),
            ("top", 120, "a", 1,
             True, (None, 250, 450, {}, {}, {}))
        )

    def test_anchor_adjust_device(self):
        [anchor] = self.parse(
            'DEF_ANCHOR "MARK_top" ON 123 GLYPH diacglyph '
            'COMPONENT 1 AT POS DX 0 DY 456 ADJUST_BY 12 AT 34 '
            'ADJUST_BY 56 AT 78 END_POS END_ANCHOR'
        ).statements
        self.assertEqual(
            (anchor.name, anchor.pos),
            ("MARK_top", (None, 0, 456, {}, {}, {34: 12, 78: 56}))
        )

    def test_ppem(self):
        [grid_ppem, pres_ppem, ppos_ppem] = self.parse(
            'GRID_PPEM 20\n'
            'PRESENTATION_PPEM 72\n'
            'PPOSITIONING_PPEM 144\n'
        ).statements
        self.assertEqual(
            ((grid_ppem.name, grid_ppem.value),
             (pres_ppem.name, pres_ppem.value),
             (ppos_ppem.name, ppos_ppem.value)),
            (("GRID_PPEM", 20), ("PRESENTATION_PPEM", 72),
             ("PPOSITIONING_PPEM", 144))
        )

    def test_compiler_flags(self):
        [setting1, setting2] = self.parse(
            'COMPILER_USEEXTENSIONLOOKUPS\n'
            'COMPILER_USEPAIRPOSFORMAT2\n'
        ).statements
        self.assertEqual(
            ((setting1.name, setting1.value),
             (setting2.name, setting2.value)),
            (("COMPILER_USEEXTENSIONLOOKUPS", True),
             ("COMPILER_USEPAIRPOSFORMAT2", True))
        )

    def test_cmap(self):
        [cmap_format1, cmap_format2, cmap_format3] = self.parse(
            'CMAP_FORMAT 0 3 4\n'
            'CMAP_FORMAT 1 0 6\n'
            'CMAP_FORMAT 3 1 4\n'
        ).statements
        self.assertEqual(
            ((cmap_format1.name, cmap_format1.value),
             (cmap_format2.name, cmap_format2.value),
             (cmap_format3.name, cmap_format3.value)),
            (("CMAP_FORMAT", (0, 3, 4)),
             ("CMAP_FORMAT", (1, 0, 6)),
             ("CMAP_FORMAT", (3, 1, 4)))
        )

    def setUp(self):
        self.tempdir = None
        self.num_tempfiles = 0

    def tearDown(self):
        if self.tempdir:
            shutil.rmtree(self.tempdir)

    def parse(self, text):
        if not self.tempdir:
            self.tempdir = tempfile.mkdtemp()
        self.num_tempfiles += 1
        path = os.path.join(self.tempdir, "tmp%d.vtp" % self.num_tempfiles)
        with open(path, "w") as outfile:
            outfile.write(text)
        return Parser(path).parse()

if __name__ == "__main__":
    import sys
    sys.exit(unittest.main())