blob: 0d8d8d289b7cdd2aa55c4de6d3c9dd47751eef17 [file] [log] [blame]
import pathlib
import shutil
import tempfile
import unittest
from io import StringIO
from fontTools.voltLib.voltToFea import VoltToFea
DATADIR = pathlib.Path(__file__).parent / "data"
class ToFeaTest(unittest.TestCase):
@classmethod
def setup_class(cls):
cls.tempdir = None
cls.num_tempfiles = 0
@classmethod
def teardown_class(cls):
if cls.tempdir:
shutil.rmtree(cls.tempdir, ignore_errors=True)
@classmethod
def temp_path(cls):
if not cls.tempdir:
cls.tempdir = pathlib.Path(tempfile.mkdtemp())
cls.num_tempfiles += 1
return cls.tempdir / f"tmp{cls.num_tempfiles}"
def test_def_glyph_base(self):
fea = self.parse('DEF_GLYPH ".notdef" ID 0 TYPE BASE END_GLYPH')
self.assertEqual(
fea,
"@GDEF_base = [.notdef];\n"
"table GDEF {\n"
" GlyphClassDef @GDEF_base, , , ;\n"
"} GDEF;\n",
)
def test_def_glyph_base_2_components(self):
fea = self.parse(
'DEF_GLYPH "glyphBase" ID 320 TYPE BASE COMPONENTS 2 END_GLYPH'
)
self.assertEqual(
fea,
"@GDEF_base = [glyphBase];\n"
"table GDEF {\n"
" GlyphClassDef @GDEF_base, , , ;\n"
"} GDEF;\n",
)
def test_def_glyph_ligature_2_components(self):
fea = self.parse('DEF_GLYPH "f_f" ID 320 TYPE LIGATURE COMPONENTS 2 END_GLYPH')
self.assertEqual(
fea,
"@GDEF_ligature = [f_f];\n"
"table GDEF {\n"
" GlyphClassDef , @GDEF_ligature, , ;\n"
"} GDEF;\n",
)
def test_def_glyph_mark(self):
fea = self.parse('DEF_GLYPH "brevecomb" ID 320 TYPE MARK END_GLYPH')
self.assertEqual(
fea,
"@GDEF_mark = [brevecomb];\n"
"table GDEF {\n"
" GlyphClassDef , , @GDEF_mark, ;\n"
"} GDEF;\n",
)
def test_def_glyph_component(self):
fea = self.parse('DEF_GLYPH "f.f_f" ID 320 TYPE COMPONENT END_GLYPH')
self.assertEqual(
fea,
"@GDEF_component = [f.f_f];\n"
"table GDEF {\n"
" GlyphClassDef , , , @GDEF_component;\n"
"} GDEF;\n",
)
def test_def_glyph_no_type(self):
fea = self.parse('DEF_GLYPH "glyph20" ID 20 END_GLYPH')
self.assertEqual(fea, "")
def test_def_glyph_case_sensitive(self):
fea = 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'
)
self.assertEqual(
fea,
"@GDEF_base = [A a];\n"
"table GDEF {\n"
" GlyphClassDef @GDEF_base, , , ;\n"
"} GDEF;\n",
)
def test_def_group_glyphs(self):
fea = 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"
)
self.assertEqual(
fea,
"# Glyph classes\n"
"@aaccented = [aacute abreve acircumflex adieresis ae"
" agrave amacron aogonek aring atilde];",
)
def test_def_group_groups(self):
fea = 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"
)
self.assertEqual(
fea,
"# Glyph classes\n"
"@Group1 = [a b c d];\n"
"@Group2 = [e f g h];\n"
"@TestGroup = [@Group1 @Group2];",
)
def test_def_group_groups_not_yet_defined(self):
fea = 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"
)
self.assertEqual(
fea,
"# Glyph classes\n"
"@Group1 = [a b c d];\n"
"@Group2 = [e f g h];\n"
"@TestGroup1 = [@Group1 @Group2];\n"
"@TestGroup2 = [@Group2];\n"
"@TestGroup3 = [@Group2 @Group1];",
)
def test_def_group_glyphs_and_group(self):
fea = 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"
)
self.assertEqual(
fea,
"# Glyph classes\n"
"@aaccented = [aacute abreve acircumflex adieresis ae"
" agrave amacron aogonek aring atilde];\n"
"@KERN_lc_a_2ND = [a @aaccented];",
)
def test_def_group_range(self):
fea = self.parse(
'DEF_GLYPH "a" ID 163 UNICODE 97 TYPE BASE END_GLYPH\n'
'DEF_GLYPH "agrave" ID 194 UNICODE 224 TYPE BASE END_GLYPH\n'
'DEF_GLYPH "aacute" ID 195 UNICODE 225 TYPE BASE END_GLYPH\n'
'DEF_GLYPH "acircumflex" ID 196 UNICODE 226 TYPE BASE END_GLYPH\n'
'DEF_GLYPH "atilde" ID 197 UNICODE 227 TYPE BASE END_GLYPH\n'
'DEF_GLYPH "c" ID 165 UNICODE 99 TYPE BASE END_GLYPH\n'
'DEF_GLYPH "ccaron" ID 209 UNICODE 269 TYPE BASE END_GLYPH\n'
'DEF_GLYPH "ccedilla" ID 210 UNICODE 231 TYPE BASE END_GLYPH\n'
'DEF_GLYPH "cdotaccent" ID 210 UNICODE 267 TYPE BASE END_GLYPH\n'
'DEF_GROUP "KERN_lc_a_2ND"\n'
'ENUM RANGE "a" TO "atilde" GLYPH "b" RANGE "c" TO "cdotaccent" '
"END_ENUM\n"
"END_GROUP"
)
self.assertEqual(
fea,
"# Glyph classes\n"
"@KERN_lc_a_2ND = [a - atilde b c - cdotaccent];\n"
"@GDEF_base = [a agrave aacute acircumflex atilde c"
" ccaron ccedilla cdotaccent];\n"
"table GDEF {\n"
" GlyphClassDef @GDEF_base, , , ;\n"
"} GDEF;\n",
)
def test_script_without_langsys(self):
fea = self.parse('DEF_SCRIPT NAME "Latin" TAG "latn"\n' "END_SCRIPT")
self.assertEqual(fea, "")
def test_langsys_normal(self):
fea = 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"
)
self.assertEqual(fea, "")
def test_langsys_no_script_name(self):
fea = self.parse(
'DEF_SCRIPT TAG "latn"\n'
'DEF_LANGSYS NAME "Default" TAG "dflt"\n'
"END_LANGSYS\n"
"END_SCRIPT"
)
self.assertEqual(fea, "")
def test_langsys_lang_in_separate_scripts(self):
fea = 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"
)
self.assertEqual(fea, "")
def test_langsys_no_lang_name(self):
fea = self.parse(
'DEF_SCRIPT NAME "Latin" TAG "latn"\n'
'DEF_LANGSYS TAG "dflt"\n'
"END_LANGSYS\n"
"END_SCRIPT"
)
self.assertEqual(fea, "")
def test_feature(self):
fea = 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\n"
'DEF_LOOKUP "fraclookup" PROCESS_BASE PROCESS_MARKS ALL '
"DIRECTION LTR\n"
"IN_CONTEXT\n"
"END_CONTEXT\n"
"AS_SUBSTITUTION\n"
'SUB GLYPH "one" GLYPH "slash" GLYPH "two"\n'
'WITH GLYPH "one_slash_two.frac"\n'
"END_SUB\n"
"END_SUBSTITUTION"
)
self.assertEqual(
fea,
"\n# Lookups\n"
"lookup fraclookup {\n"
" sub one slash two by one_slash_two.frac;\n"
"} fraclookup;\n"
"\n"
"# Features\n"
"feature frac {\n"
" script latn;\n"
" language ROM exclude_dflt;\n"
" lookup fraclookup;\n"
"} frac;\n",
)
def test_feature_sub_lookups(self):
fea = 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\\1"\n'
'LOOKUP "fraclookup\\1"\n'
"END_FEATURE\n"
"END_LANGSYS\n"
"END_SCRIPT\n"
'DEF_LOOKUP "fraclookup\\1" PROCESS_BASE PROCESS_MARKS ALL '
"DIRECTION RTL\n"
"IN_CONTEXT\n"
"END_CONTEXT\n"
"AS_SUBSTITUTION\n"
'SUB GLYPH "one" GLYPH "slash" GLYPH "two"\n'
'WITH GLYPH "one_slash_two.frac"\n'
"END_SUB\n"
"END_SUBSTITUTION\n"
'DEF_LOOKUP "fraclookup\\2" PROCESS_BASE PROCESS_MARKS ALL '
"DIRECTION RTL\n"
"IN_CONTEXT\n"
"END_CONTEXT\n"
"AS_SUBSTITUTION\n"
'SUB GLYPH "one" GLYPH "slash" GLYPH "three"\n'
'WITH GLYPH "one_slash_three.frac"\n'
"END_SUB\n"
"END_SUBSTITUTION"
)
self.assertEqual(
fea,
"\n# Lookups\n"
"lookup fraclookup {\n"
" lookupflag RightToLeft;\n"
" # fraclookup\\1\n"
" sub one slash two by one_slash_two.frac;\n"
" subtable;\n"
" # fraclookup\\2\n"
" sub one slash three by one_slash_three.frac;\n"
"} fraclookup;\n"
"\n"
"# Features\n"
"feature frac {\n"
" script latn;\n"
" language ROM exclude_dflt;\n"
" lookup fraclookup;\n"
"} frac;\n",
)
def test_lookup_comment(self):
fea = self.parse(
'DEF_LOOKUP "smcp" PROCESS_BASE PROCESS_MARKS ALL '
"DIRECTION LTR\n"
'COMMENTS "Smallcaps lookup for testing"\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"
)
self.assertEqual(
fea,
"\n# Lookups\n"
"lookup smcp {\n"
" # Smallcaps lookup for testing\n"
" sub a by a.sc;\n"
" sub b by b.sc;\n"
"} smcp;\n",
)
def test_substitution_single(self):
fea = 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"
"SUB WITH\n" # Empty substitution, will be ignored
"END_SUB\n"
"END_SUBSTITUTION"
)
self.assertEqual(
fea,
"\n# Lookups\n"
"lookup smcp {\n"
" sub a by a.sc;\n"
" sub b by b.sc;\n"
"} smcp;\n",
)
def test_substitution_single_in_context(self):
fea = 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"
)
self.assertEqual(
fea,
"# Glyph classes\n"
"@Denominators = [one.dnom two.dnom];\n"
"\n"
"# Lookups\n"
"lookup fracdnom {\n"
" sub [@Denominators fraction] one' by one.dnom;\n"
" sub [@Denominators fraction] two' by two.dnom;\n"
"} fracdnom;\n",
)
def test_substitution_single_in_contexts(self):
fea = 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"
)
self.assertEqual(
fea,
"# Glyph classes\n"
"@Hebrew = [uni05D0 uni05D1];\n"
"\n"
"# Lookups\n"
"lookup HebrewCurrency {\n"
" sub dollar' @Hebrew one.Hebr by dollar.Hebr;\n"
" sub @Hebrew one.Hebr dollar' by dollar.Hebr;\n"
"} HebrewCurrency;\n",
)
def test_substitution_single_except_context(self):
fea = 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"
"EXCEPT_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"
)
self.assertEqual(
fea,
"# Glyph classes\n"
"@Hebrew = [uni05D0 uni05D1];\n"
"\n"
"# Lookups\n"
"lookup HebrewCurrency {\n"
" ignore sub dollar' @Hebrew one.Hebr;\n"
" sub @Hebrew one.Hebr dollar' by dollar.Hebr;\n"
"} HebrewCurrency;\n",
)
def test_substitution_skip_base(self):
fea = 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"
)
self.assertEqual(
fea,
"# Glyph classes\n"
"@SomeMarks = [marka markb];\n"
"\n"
"# Lookups\n"
"lookup SomeSub {\n"
" lookupflag IgnoreBaseGlyphs;\n"
" sub A by A.c2sc;\n"
"} SomeSub;\n",
)
def test_substitution_process_base(self):
fea = 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"
)
self.assertEqual(
fea,
"# Glyph classes\n"
"@SomeMarks = [marka markb];\n"
"\n"
"# Lookups\n"
"lookup SomeSub {\n"
" sub A by A.c2sc;\n"
"} SomeSub;\n",
)
def test_substitution_process_marks_all(self):
fea = 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"
)
self.assertEqual(
fea,
"# Glyph classes\n"
"@SomeMarks = [marka markb];\n"
"\n"
"# Lookups\n"
"lookup SomeSub {\n"
" sub A by A.c2sc;\n"
"} SomeSub;\n",
)
def test_substitution_process_marks_none(self):
fea = self.parse(
'DEF_GROUP "SomeMarks" ENUM GLYPH "marka" GLYPH "markb" '
"END_ENUM END_GROUP\n"
'DEF_LOOKUP "SomeSub" PROCESS_BASE PROCESS_MARKS "NONE"'
"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"
)
self.assertEqual(
fea,
"# Glyph classes\n"
"@SomeMarks = [marka markb];\n"
"\n"
"# Lookups\n"
"lookup SomeSub {\n"
" lookupflag IgnoreMarks;\n"
" sub A by A.c2sc;\n"
"} SomeSub;\n",
)
def test_substitution_skip_marks(self):
fea = 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"
)
self.assertEqual(
fea,
"# Glyph classes\n"
"@SomeMarks = [marka markb];\n"
"\n"
"# Lookups\n"
"lookup SomeSub {\n"
" lookupflag IgnoreMarks;\n"
" sub A by A.c2sc;\n"
"} SomeSub;\n",
)
def test_substitution_mark_attachment(self):
fea = 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"
)
self.assertEqual(
fea,
"# Glyph classes\n"
"@SomeMarks = [acutecmb gravecmb];\n"
"\n"
"# Lookups\n"
"lookup SomeSub {\n"
" lookupflag RightToLeft MarkAttachmentType"
" @SomeMarks;\n"
" sub A by A.c2sc;\n"
"} SomeSub;\n",
)
def test_substitution_mark_glyph_set(self):
fea = self.parse(
'DEF_GROUP "SomeMarks" ENUM GLYPH "acutecmb" GLYPH "gravecmb" '
"END_ENUM END_GROUP\n"
'DEF_LOOKUP "SomeSub" PROCESS_BASE '
'PROCESS_MARKS MARK_GLYPH_SET "SomeMarks" \n'
"DIRECTION RTL\n"
"AS_SUBSTITUTION\n"
'SUB GLYPH "A"\n'
'WITH GLYPH "A.c2sc"\n'
"END_SUB\n"
"END_SUBSTITUTION"
)
self.assertEqual(
fea,
"# Glyph classes\n"
"@SomeMarks = [acutecmb gravecmb];\n"
"\n"
"# Lookups\n"
"lookup SomeSub {\n"
" lookupflag RightToLeft UseMarkFilteringSet"
" @SomeMarks;\n"
" sub A by A.c2sc;\n"
"} SomeSub;\n",
)
def test_substitution_process_all_marks(self):
fea = 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"
)
self.assertEqual(
fea,
"# Glyph classes\n"
"@SomeMarks = [acutecmb gravecmb];\n"
"\n"
"# Lookups\n"
"lookup SomeSub {\n"
" lookupflag RightToLeft;\n"
" sub A by A.c2sc;\n"
"} SomeSub;\n",
)
def test_substitution_no_reversal(self):
# TODO: check right context with no reversal
fea = 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"
)
self.assertEqual(
fea,
"\n# Lookups\n"
"lookup Lookup {\n"
" sub a' [a b] by a.alt;\n"
"} Lookup;\n",
)
def test_substitution_reversal(self):
fea = self.parse(
'DEF_GROUP "DFLT_Num_standardFigures"\n'
'ENUM GLYPH "zero" GLYPH "one" GLYPH "two" END_ENUM\n'
"END_GROUP\n"
'DEF_GROUP "DFLT_Num_numerators"\n'
'ENUM GLYPH "zero.numr" GLYPH "one.numr" GLYPH "two.numr" END_ENUM\n'
"END_GROUP\n"
'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"
)
self.assertEqual(
fea,
"# Glyph classes\n"
"@DFLT_Num_standardFigures = [zero one two];\n"
"@DFLT_Num_numerators = [zero.numr one.numr two.numr];\n"
"\n"
"# Lookups\n"
"lookup RevLookup {\n"
" rsub @DFLT_Num_standardFigures' [a b] by @DFLT_Num_numerators;\n"
"} RevLookup;\n",
)
def test_substitution_single_to_multiple(self):
fea = 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"
)
self.assertEqual(
fea,
"\n# Lookups\n"
"lookup ccmp {\n"
" sub aacute by a acutecomb;\n"
" sub agrave by a gravecomb;\n"
"} ccmp;\n",
)
def test_substitution_multiple_to_single(self):
fea = 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"
)
self.assertEqual(
fea,
"\n# Lookups\n"
"lookup liga {\n"
" sub f i by f_i;\n"
" sub f t by f_t;\n"
"} liga;\n",
)
def test_substitution_reverse_chaining_single(self):
fea = 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"
)
self.assertEqual(
fea,
"\n# Lookups\n"
"lookup numr {\n"
" rsub zero - nine' [fraction zero.numr - nine.numr] by zero.numr - nine.numr;\n"
"} numr;\n",
)
# GPOS
# ATTACH_CURSIVE
# ATTACH
# ADJUST_PAIR
# ADJUST_SINGLE
def test_position_attach(self):
fea = 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"
)
self.assertEqual(
fea,
"\n# Mark classes\n"
"markClass acutecomb <anchor 0 450> @top;\n"
"markClass gravecomb <anchor 0 450> @top;\n"
"\n"
"# Lookups\n"
"lookup anchor_top {\n"
" lookupflag RightToLeft;\n"
" pos base a\n"
" <anchor 210 450> mark @top;\n"
" pos base e\n"
" <anchor 215 450> mark @top;\n"
"} anchor_top;\n",
)
def test_position_attach_mkmk(self):
fea = self.parse(
'DEF_GLYPH "brevecomb" ID 1 TYPE MARK END_GLYPH\n'
'DEF_GLYPH "gravecomb" ID 2 TYPE MARK END_GLYPH\n'
'DEF_LOOKUP "anchor_top" PROCESS_BASE PROCESS_MARKS ALL '
"DIRECTION RTL\n"
"IN_CONTEXT\n"
"END_CONTEXT\n"
"AS_POSITION\n"
'ATTACH GLYPH "gravecomb"\n'
'TO GLYPH "acutecomb" AT ANCHOR "top"\n'
"END_ATTACH\n"
"END_POSITION\n"
'DEF_ANCHOR "MARK_top" ON 1 GLYPH acutecomb COMPONENT 1 '
"AT POS DX 0 DY 450 END_POS END_ANCHOR\n"
'DEF_ANCHOR "top" ON 2 GLYPH gravecomb COMPONENT 1 '
"AT POS DX 210 DY 450 END_POS END_ANCHOR\n"
)
self.assertEqual(
fea,
"\n# Mark classes\n"
"markClass acutecomb <anchor 0 450> @top;\n"
"\n"
"# Lookups\n"
"lookup anchor_top {\n"
" lookupflag RightToLeft;\n"
" pos mark gravecomb\n"
" <anchor 210 450> mark @top;\n"
"} anchor_top;\n"
"\n"
"@GDEF_mark = [brevecomb gravecomb];\n"
"table GDEF {\n"
" GlyphClassDef , , @GDEF_mark, ;\n"
"} GDEF;\n",
)
def test_position_attach_in_context(self):
fea = self.parse(
'DEF_LOOKUP "test" PROCESS_BASE PROCESS_MARKS ALL '
"DIRECTION RTL\n"
'EXCEPT_CONTEXT LEFT GLYPH "a" END_CONTEXT\n'
"AS_POSITION\n"
'ATTACH GLYPH "a"\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"
)
self.assertEqual(
fea,
"\n# Mark classes\n"
"markClass acutecomb <anchor 0 450> @top;\n"
"markClass gravecomb <anchor 0 450> @top;\n"
"\n"
"# Lookups\n"
"lookup test_target {\n"
" pos base a\n"
" <anchor 210 450> mark @top;\n"
"} test_target;\n"
"\n"
"lookup test {\n"
" lookupflag RightToLeft;\n"
" ignore pos a [acutecomb gravecomb]';\n"
" pos [acutecomb gravecomb]' lookup test_target;\n"
"} test;\n",
)
def test_position_attach_cursive(self):
fea = 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 "a" GLYPH "c"\n'
"END_ATTACH\n"
"END_POSITION\n"
'DEF_ANCHOR "exit" ON 1 GLYPH a COMPONENT 1 AT POS END_POS END_ANCHOR\n'
'DEF_ANCHOR "entry" ON 1 GLYPH a COMPONENT 1 AT POS END_POS END_ANCHOR\n'
'DEF_ANCHOR "exit" ON 2 GLYPH b COMPONENT 1 AT POS END_POS END_ANCHOR\n'
'DEF_ANCHOR "entry" ON 3 GLYPH c COMPONENT 1 AT POS END_POS END_ANCHOR\n'
)
self.assertEqual(
fea,
"\n# Lookups\n"
"lookup SomeLookup {\n"
" lookupflag RightToLeft;\n"
" pos cursive a <anchor 0 0> <anchor 0 0>;\n"
" pos cursive c <anchor 0 0> <anchor NULL>;\n"
" pos cursive b <anchor NULL> <anchor 0 0>;\n"
"} SomeLookup;\n",
)
def test_position_adjust_pair(self):
fea = 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" FIRST GLYPH "V"\n'
' SECOND GLYPH "A" SECOND GLYPH "V"\n'
" 1 2 BY POS ADV -30 END_POS POS END_POS\n"
" 2 1 BY POS ADV -25 END_POS POS END_POS\n"
"END_ADJUST\n"
"END_POSITION\n"
)
self.assertEqual(
fea,
"\n# Lookups\n"
"lookup kern1 {\n"
" lookupflag RightToLeft;\n"
" enum pos A V -30;\n"
" enum pos V A -25;\n"
"} kern1;\n",
)
def test_position_adjust_pair_in_context(self):
fea = self.parse(
'DEF_LOOKUP "kern1" PROCESS_BASE PROCESS_MARKS ALL '
"DIRECTION LTR\n"
'EXCEPT_CONTEXT LEFT GLYPH "A" END_CONTEXT\n'
"AS_POSITION\n"
"ADJUST_PAIR\n"
' FIRST GLYPH "A" FIRST GLYPH "V"\n'
' SECOND GLYPH "A" SECOND GLYPH "V"\n'
" 2 1 BY POS ADV -25 END_POS POS END_POS\n"
"END_ADJUST\n"
"END_POSITION\n"
)
self.assertEqual(
fea,
"\n# Lookups\n"
"lookup kern1_target {\n"
" enum pos V A -25;\n"
"} kern1_target;\n"
"\n"
"lookup kern1 {\n"
" ignore pos A V' A';\n"
" pos V' lookup kern1_target A' lookup kern1_target;\n"
"} kern1;\n",
)
def test_position_adjust_single(self):
fea = self.parse(
'DEF_LOOKUP "TestLookup" PROCESS_BASE PROCESS_MARKS ALL '
"DIRECTION LTR\n"
"IN_CONTEXT\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"
)
self.assertEqual(
fea,
"\n# Lookups\n"
"lookup TestLookup {\n"
" pos glyph1 <123 0 0 0>;\n"
" pos glyph2 <456 0 0 0>;\n"
"} TestLookup;\n",
)
def test_position_adjust_single_in_context(self):
fea = self.parse(
'DEF_LOOKUP "TestLookup" PROCESS_BASE PROCESS_MARKS ALL '
"DIRECTION LTR\n"
"EXCEPT_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"
)
self.assertEqual(
fea,
"\n# Lookups\n"
"lookup TestLookup_target {\n"
" pos glyph1 <123 0 0 0>;\n"
" pos glyph2 <456 0 0 0>;\n"
"} TestLookup_target;\n"
"\n"
"lookup TestLookup {\n"
" ignore pos leftGlyph [glyph1 glyph2]' rightGlyph;\n"
" pos [glyph1 glyph2]' lookup TestLookup_target;\n"
"} TestLookup;\n",
)
def test_def_anchor(self):
fea = self.parse(
'DEF_LOOKUP "TestLookup" PROCESS_BASE PROCESS_MARKS ALL '
"DIRECTION LTR\n"
"IN_CONTEXT\n"
"END_CONTEXT\n"
"AS_POSITION\n"
'ATTACH GLYPH "a"\n'
'TO GLYPH "acutecomb" AT ANCHOR "top"\n'
"END_ATTACH\n"
"END_POSITION\n"
'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"
)
self.assertEqual(
fea,
"\n# Mark classes\n"
"markClass acutecomb <anchor 0 450> @top;\n"
"\n"
"# Lookups\n"
"lookup TestLookup {\n"
" pos base a\n"
" <anchor 250 450> mark @top;\n"
"} TestLookup;\n",
)
def test_def_anchor_multi_component(self):
fea = self.parse(
'DEF_LOOKUP "TestLookup" PROCESS_BASE PROCESS_MARKS ALL '
"DIRECTION LTR\n"
"IN_CONTEXT\n"
"END_CONTEXT\n"
"AS_POSITION\n"
'ATTACH GLYPH "f_f"\n'
'TO GLYPH "acutecomb" AT ANCHOR "top"\n'
"END_ATTACH\n"
"END_POSITION\n"
'DEF_GLYPH "f_f" ID 120 TYPE LIGATURE COMPONENTS 2 END_GLYPH\n'
'DEF_ANCHOR "top" ON 120 GLYPH f_f '
"COMPONENT 1 AT POS DX 250 DY 450 END_POS END_ANCHOR\n"
'DEF_ANCHOR "top" ON 120 GLYPH f_f '
"COMPONENT 2 AT POS DX 450 DY 450 END_POS END_ANCHOR\n"
'DEF_ANCHOR "MARK_top" ON 120 GLYPH acutecomb '
"COMPONENT 1 AT POS END_POS END_ANCHOR"
)
self.assertEqual(
fea,
"\n# Mark classes\n"
"markClass acutecomb <anchor 0 0> @top;\n"
"\n"
"# Lookups\n"
"lookup TestLookup {\n"
" pos ligature f_f\n"
" <anchor 250 450> mark @top\n"
" ligComponent\n"
" <anchor 450 450> mark @top;\n"
"} TestLookup;\n"
"\n"
"@GDEF_ligature = [f_f];\n"
"table GDEF {\n"
" GlyphClassDef , @GDEF_ligature, , ;\n"
"} GDEF;\n",
)
def test_anchor_adjust_device(self):
fea = 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"
)
self.assertEqual(
fea,
"\n# Mark classes\n"
"#markClass diacglyph <anchor 0 456 <device NULL>"
" <device 34 12, 78 56>> @top;",
)
def test_use_extension(self):
fea = self.parse(
'DEF_LOOKUP "kern1" PROCESS_BASE PROCESS_MARKS ALL '
"DIRECTION LTR\n"
"IN_CONTEXT\n"
"END_CONTEXT\n"
"AS_POSITION\n"
"ADJUST_PAIR\n"
' FIRST GLYPH "A" FIRST GLYPH "V"\n'
' SECOND GLYPH "A" SECOND GLYPH "V"\n'
" 1 2 BY POS ADV -30 END_POS POS END_POS\n"
" 2 1 BY POS ADV -25 END_POS POS END_POS\n"
"END_ADJUST\n"
"END_POSITION\n"
"COMPILER_USEEXTENSIONLOOKUPS\n"
)
self.assertEqual(
fea,
"\n# Lookups\n"
"lookup kern1 useExtension {\n"
" enum pos A V -30;\n"
" enum pos V A -25;\n"
"} kern1;\n",
)
def test_unsupported_compiler_flags(self):
with self.assertLogs(level="WARNING") as logs:
fea = self.parse("CMAP_FORMAT 0 3 4")
self.assertEqual(fea, "")
self.assertEqual(
logs.output,
[
"WARNING:fontTools.voltLib.voltToFea:Unsupported setting ignored: CMAP_FORMAT"
],
)
def test_sanitize_lookup_name(self):
fea = self.parse(
'DEF_LOOKUP "Test Lookup" PROCESS_BASE PROCESS_MARKS ALL '
"DIRECTION LTR IN_CONTEXT END_CONTEXT\n"
"AS_POSITION ADJUST_PAIR END_ADJUST END_POSITION\n"
'DEF_LOOKUP "Test-Lookup" PROCESS_BASE PROCESS_MARKS ALL '
"DIRECTION LTR IN_CONTEXT END_CONTEXT\n"
"AS_POSITION ADJUST_PAIR END_ADJUST END_POSITION\n"
)
self.assertEqual(
fea,
"\n# Lookups\n"
"lookup Test_Lookup {\n"
" \n"
"} Test_Lookup;\n"
"\n"
"lookup Test_Lookup_ {\n"
" \n"
"} Test_Lookup_;\n",
)
def test_sanitize_group_name(self):
fea = self.parse(
'DEF_GROUP "aaccented glyphs"\n'
'ENUM GLYPH "aacute" GLYPH "abreve" END_ENUM\n'
"END_GROUP\n"
'DEF_GROUP "aaccented+glyphs"\n'
'ENUM GLYPH "aacute" GLYPH "abreve" END_ENUM\n'
"END_GROUP\n"
)
self.assertEqual(
fea,
"# Glyph classes\n"
"@aaccented_glyphs = [aacute abreve];\n"
"@aaccented_glyphs_ = [aacute abreve];",
)
def test_cli_vtp(self):
vtp = DATADIR / "Nutso.vtp"
fea = DATADIR / "Nutso.fea"
self.cli(vtp, fea)
def test_group_order(self):
vtp = DATADIR / "NamdhinggoSIL1006.vtp"
fea = DATADIR / "NamdhinggoSIL1006.fea"
self.cli(vtp, fea)
def test_cli_ttf(self):
ttf = DATADIR / "Nutso.ttf"
fea = DATADIR / "Nutso.fea"
self.cli(ttf, fea)
def test_cli_ttf_no_TSIV(self):
from fontTools.voltLib.voltToFea import main as cli
ttf = DATADIR / "Empty.ttf"
temp = self.temp_path()
self.assertEqual(1, cli([str(ttf), str(temp)]))
def cli(self, source, fea):
from fontTools.voltLib.voltToFea import main as cli
temp = self.temp_path()
cli([str(source), str(temp)])
with temp.open() as f:
res = f.read()
with fea.open() as f:
ref = f.read()
self.assertEqual(ref, res)
def parse(self, text):
return VoltToFea(StringIO(text)).convert()
if __name__ == "__main__":
import sys
sys.exit(unittest.main())