diff options
Diffstat (limited to 'Tests/feaLib/builder_test.py')
-rw-r--r-- | Tests/feaLib/builder_test.py | 531 |
1 files changed, 334 insertions, 197 deletions
diff --git a/Tests/feaLib/builder_test.py b/Tests/feaLib/builder_test.py index 0a55239c..5c298e85 100644 --- a/Tests/feaLib/builder_test.py +++ b/Tests/feaLib/builder_test.py @@ -1,11 +1,15 @@ from fontTools.misc.loggingTools import CapturingLogHandler -from fontTools.feaLib.builder import Builder, addOpenTypeFeatures, \ - addOpenTypeFeaturesFromString +from fontTools.feaLib.builder import ( + Builder, + addOpenTypeFeatures, + addOpenTypeFeaturesFromString, +) from fontTools.feaLib.error import FeatureLibError -from fontTools.ttLib import TTFont +from fontTools.ttLib import TTFont, newTable from fontTools.feaLib.parser import Parser from fontTools.feaLib import ast from fontTools.feaLib.lexer import Lexer +from fontTools.fontBuilder import addFvar import difflib from io import StringIO import os @@ -65,7 +69,7 @@ class BuilderTest(unittest.TestCase): spec9a spec9b spec9c1 spec9c2 spec9c3 spec9d spec9e spec9f spec9g spec10 bug453 bug457 bug463 bug501 bug502 bug504 bug505 bug506 bug509 - bug512 bug514 bug568 bug633 bug1307 bug1459 + bug512 bug514 bug568 bug633 bug1307 bug1459 bug2276 name size size2 multiple_feature_blocks omitted_GlyphClassDef ZeroValue_SinglePos_horizontal ZeroValue_SinglePos_vertical ZeroValue_PairPos_horizontal ZeroValue_PairPos_vertical @@ -75,8 +79,14 @@ class BuilderTest(unittest.TestCase): SingleSubstSubtable aalt_chain_contextual_subst AlternateChained MultipleLookupsPerGlyph MultipleLookupsPerGlyph2 GSUB_6_formats GSUB_5_formats delete_glyph STAT_test STAT_test_elidedFallbackNameID + variable_scalar_valuerecord variable_scalar_anchor variable_conditionset """.split() + VARFONT_AXES = [ + ("wght", 200, 200, 1000, "Weight"), + ("wdth", 100, 100, 200, "Width"), + ] + def __init__(self, methodName): unittest.TestCase.__init__(self, methodName) # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, @@ -101,25 +111,36 @@ class BuilderTest(unittest.TestCase): if not self.tempdir: self.tempdir = tempfile.mkdtemp() self.num_tempfiles += 1 - return os.path.join(self.tempdir, - "tmp%d%s" % (self.num_tempfiles, suffix)) + return os.path.join(self.tempdir, "tmp%d%s" % (self.num_tempfiles, suffix)) def read_ttx(self, path): lines = [] with open(path, "r", encoding="utf-8") as ttx: for line in ttx.readlines(): - # Elide ttFont attributes because ttLibVersion may change, - # and use os-native line separators so we can run difflib. + # Elide ttFont attributes because ttLibVersion may change. if line.startswith("<ttFont "): - lines.append("<ttFont>" + os.linesep) + lines.append("<ttFont>\n") else: - lines.append(line.rstrip() + os.linesep) + lines.append(line.rstrip() + "\n") return lines def expect_ttx(self, font, expected_ttx, replace=None): path = self.temp_path(suffix=".ttx") - font.saveXML(path, tables=['head', 'name', 'BASE', 'GDEF', 'GSUB', - 'GPOS', 'OS/2', 'STAT', 'hhea', 'vhea']) + font.saveXML( + path, + tables=[ + "head", + "name", + "BASE", + "GDEF", + "GSUB", + "GPOS", + "OS/2", + "STAT", + "hhea", + "vhea", + ], + ) actual = self.read_ttx(path) expected = self.read_ttx(expected_ttx) if replace: @@ -128,7 +149,8 @@ class BuilderTest(unittest.TestCase): expected[i] = expected[i].replace(k, v) if actual != expected: for line in difflib.unified_diff( - expected, actual, fromfile=expected_ttx, tofile=path): + expected, actual, fromfile=expected_ttx, tofile=path + ): sys.stderr.write(line) self.fail("TTX output is different from expected") @@ -139,13 +161,17 @@ class BuilderTest(unittest.TestCase): def check_feature_file(self, name): font = makeTTFont() + if name.startswith("variable_"): + font["name"] = newTable("name") + addFvar(font, self.VARFONT_AXES, []) + del font["name"] feapath = self.getpath("%s.fea" % name) addOpenTypeFeatures(font, feapath) self.expect_ttx(font, self.getpath("%s.ttx" % name)) # Check that: # 1) tables do compile (only G* tables as long as we have a mock font) # 2) dumping after save-reload yields the same TTX dump as before - for tag in ('GDEF', 'GSUB', 'GPOS'): + for tag in ("GDEF", "GSUB", "GPOS"): if tag in font: data = font[tag].compile(font) font[tag].decompile(data, font) @@ -154,11 +180,11 @@ class BuilderTest(unittest.TestCase): debugttx = self.getpath("%s-debug.ttx" % name) if os.path.exists(debugttx): addOpenTypeFeatures(font, feapath, debug=True) - self.expect_ttx(font, debugttx, replace = {"__PATH__": feapath}) + self.expect_ttx(font, debugttx, replace={"__PATH__": feapath}) def check_fea2fea_file(self, name, base=None, parser=Parser): font = makeTTFont() - fname = (name + ".fea") if '.' not in name else name + fname = (name + ".fea") if "." not in name else name p = parser(self.getpath(fname), glyphNames=font.getGlyphOrder()) doc = p.parse() actual = self.normal_fea(doc.asFea().split("\n")) @@ -168,12 +194,16 @@ class BuilderTest(unittest.TestCase): if expected != actual: fname = name.rsplit(".", 1)[0] + ".fea" for line in difflib.unified_diff( - expected, actual, - fromfile=fname + " (expected)", - tofile=fname + " (actual)"): - sys.stderr.write(line+"\n") - self.fail("Fea2Fea output is different from expected. " - "Generated:\n{}\n".format("\n".join(actual))) + expected, + actual, + fromfile=fname + " (expected)", + tofile=fname + " (actual)", + ): + sys.stderr.write(line + "\n") + self.fail( + "Fea2Fea output is different from expected. " + "Generated:\n{}\n".format("\n".join(actual)) + ) def normal_fea(self, lines): output = [] @@ -198,13 +228,14 @@ class BuilderTest(unittest.TestCase): def test_alternateSubst_multipleSubstitutionsForSameGlyph(self): self.assertRaisesRegex( FeatureLibError, - "Already defined alternates for glyph \"A\"", + 'Already defined alternates for glyph "A"', self.build, "feature test {" " sub A from [A.alt1 A.alt2];" " sub B from [B.alt1 B.alt2 B.alt3];" " sub A from [A.alt1 A.alt2];" - "} test;") + "} test;", + ) def test_singleSubst_multipleIdenticalSubstitutionsForSameGlyph_info(self): logger = logging.getLogger("fontTools.feaLib.builder") @@ -214,19 +245,23 @@ class BuilderTest(unittest.TestCase): " sub A by A.sc;" " sub B by B.sc;" " sub A by A.sc;" - "} test;") - captor.assertRegex('Removing duplicate single substitution from glyph "A" to "A.sc"') + "} test;" + ) + captor.assertRegex( + 'Removing duplicate single substitution from glyph "A" to "A.sc"' + ) def test_multipleSubst_multipleSubstitutionsForSameGlyph(self): self.assertRaisesRegex( FeatureLibError, - "Already defined substitution for glyph \"f_f_i\"", + 'Already defined substitution for glyph "f_f_i"', self.build, "feature test {" " sub f_f_i by f f i;" " sub c_t by c t;" " sub f_f_i by f_f i;" - "} test;") + "} test;", + ) def test_multipleSubst_multipleIdenticalSubstitutionsForSameGlyph_info(self): logger = logging.getLogger("fontTools.feaLib.builder") @@ -236,8 +271,11 @@ class BuilderTest(unittest.TestCase): " sub f_f_i by f f i;" " sub c_t by c t;" " sub f_f_i by f f i;" - "} test;") - captor.assertRegex(r"Removing duplicate multiple substitution from glyph \"f_f_i\" to \('f', 'f', 'i'\)") + "} test;" + ) + captor.assertRegex( + r"Removing duplicate multiple substitution from glyph \"f_f_i\" to \('f', 'f', 'i'\)" + ) def test_pairPos_redefinition_warning(self): # https://github.com/fonttools/fonttools/issues/1147 @@ -251,17 +289,18 @@ class BuilderTest(unittest.TestCase): " pos yacute semicolon -70;" " enum pos @Y_LC semicolon -80;" " pos @Y_LC @SMALL_PUNC -100;" - "} kern;") + "} kern;" + ) captor.assertRegex("Already defined position for pair yacute semicolon") # the first definition prevails: yacute semicolon -70 st = font["GPOS"].table.LookupList.Lookup[0].SubTable[0] self.assertEqual(st.Coverage.glyphs[2], "yacute") - self.assertEqual(st.PairSet[2].PairValueRecord[0].SecondGlyph, - "semicolon") - self.assertEqual(vars(st.PairSet[2].PairValueRecord[0].Value1), - {"XAdvance": -70}) + self.assertEqual(st.PairSet[2].PairValueRecord[0].SecondGlyph, "semicolon") + self.assertEqual( + vars(st.PairSet[2].PairValueRecord[0].Value1), {"XAdvance": -70} + ) def test_singleSubst_multipleSubstitutionsForSameGlyph(self): self.assertRaisesRegex( @@ -271,127 +310,153 @@ class BuilderTest(unittest.TestCase): "feature test {" " sub [a-z] by [A.sc-Z.sc];" " sub e by e.fina;" - "} test;") + "} test;", + ) def test_singlePos_redefinition(self): self.assertRaisesRegex( FeatureLibError, - "Already defined different position for glyph \"A\"", - self.build, "feature test { pos A 123; pos A 456; } test;") + 'Already defined different position for glyph "A"', + self.build, + "feature test { pos A 123; pos A 456; } test;", + ) def test_feature_outside_aalt(self): self.assertRaisesRegex( FeatureLibError, 'Feature references are only allowed inside "feature aalt"', - self.build, "feature test { feature test; } test;") + self.build, + "feature test { feature test; } test;", + ) def test_feature_undefinedReference(self): self.assertRaisesRegex( - FeatureLibError, 'Feature none has not been defined', - self.build, "feature aalt { feature none; } aalt;") + FeatureLibError, + "Feature none has not been defined", + self.build, + "feature aalt { feature none; } aalt;", + ) def test_GlyphClassDef_conflictingClasses(self): self.assertRaisesRegex( - FeatureLibError, "Glyph X was assigned to a different class", + FeatureLibError, + "Glyph X was assigned to a different class", self.build, "table GDEF {" " GlyphClassDef [a b], [X], , ;" " GlyphClassDef [a b X], , , ;" - "} GDEF;") + "} GDEF;", + ) def test_languagesystem(self): builder = Builder(makeTTFont(), (None, None)) - builder.add_language_system(None, 'latn', 'FRA') - builder.add_language_system(None, 'cyrl', 'RUS') - builder.start_feature(location=None, name='test') - self.assertEqual(builder.language_systems, - {('latn', 'FRA'), ('cyrl', 'RUS')}) + builder.add_language_system(None, "latn", "FRA") + builder.add_language_system(None, "cyrl", "RUS") + builder.start_feature(location=None, name="test") + self.assertEqual(builder.language_systems, {("latn", "FRA"), ("cyrl", "RUS")}) def test_languagesystem_duplicate(self): self.assertRaisesRegex( FeatureLibError, '"languagesystem cyrl RUS" has already been specified', - self.build, "languagesystem cyrl RUS; languagesystem cyrl RUS;") + self.build, + "languagesystem cyrl RUS; languagesystem cyrl RUS;", + ) def test_languagesystem_none_specified(self): builder = Builder(makeTTFont(), (None, None)) - builder.start_feature(location=None, name='test') - self.assertEqual(builder.language_systems, {('DFLT', 'dflt')}) + builder.start_feature(location=None, name="test") + self.assertEqual(builder.language_systems, {("DFLT", "dflt")}) def test_languagesystem_DFLT_dflt_not_first(self): self.assertRaisesRegex( FeatureLibError, - "If \"languagesystem DFLT dflt\" is present, " + 'If "languagesystem DFLT dflt" is present, ' "it must be the first of the languagesystem statements", - self.build, "languagesystem latn TRK; languagesystem DFLT dflt;") + self.build, + "languagesystem latn TRK; languagesystem DFLT dflt;", + ) def test_languagesystem_DFLT_not_preceding(self): self.assertRaisesRegex( FeatureLibError, - "languagesystems using the \"DFLT\" script tag must " + 'languagesystems using the "DFLT" script tag must ' "precede all other languagesystems", self.build, "languagesystem DFLT dflt; " "languagesystem latn dflt; " - "languagesystem DFLT fooo; " + "languagesystem DFLT fooo; ", ) def test_script(self): builder = Builder(makeTTFont(), (None, None)) - builder.start_feature(location=None, name='test') - builder.set_script(location=None, script='cyrl') - self.assertEqual(builder.language_systems, {('cyrl', 'dflt')}) + builder.start_feature(location=None, name="test") + builder.set_script(location=None, script="cyrl") + self.assertEqual(builder.language_systems, {("cyrl", "dflt")}) def test_script_in_aalt_feature(self): self.assertRaisesRegex( FeatureLibError, - "Script statements are not allowed within \"feature aalt\"", - self.build, "feature aalt { script latn; } aalt;") + 'Script statements are not allowed within "feature aalt"', + self.build, + "feature aalt { script latn; } aalt;", + ) def test_script_in_size_feature(self): self.assertRaisesRegex( FeatureLibError, - "Script statements are not allowed within \"feature size\"", - self.build, "feature size { script latn; } size;") + 'Script statements are not allowed within "feature size"', + self.build, + "feature size { script latn; } size;", + ) def test_script_in_standalone_lookup(self): self.assertRaisesRegex( FeatureLibError, "Script statements are not allowed within standalone lookup blocks", - self.build, "lookup test { script latn; } test;") + self.build, + "lookup test { script latn; } test;", + ) def test_language(self): builder = Builder(makeTTFont(), (None, None)) - builder.add_language_system(None, 'latn', 'FRA ') - builder.start_feature(location=None, name='test') - builder.set_script(location=None, script='cyrl') - builder.set_language(location=None, language='RUS ', - include_default=False, required=False) - self.assertEqual(builder.language_systems, {('cyrl', 'RUS ')}) - builder.set_language(location=None, language='BGR ', - include_default=True, required=False) - self.assertEqual(builder.language_systems, - {('cyrl', 'BGR ')}) - builder.start_feature(location=None, name='test2') - self.assertEqual(builder.language_systems, {('latn', 'FRA ')}) + builder.add_language_system(None, "latn", "FRA ") + builder.start_feature(location=None, name="test") + builder.set_script(location=None, script="cyrl") + builder.set_language( + location=None, language="RUS ", include_default=False, required=False + ) + self.assertEqual(builder.language_systems, {("cyrl", "RUS ")}) + builder.set_language( + location=None, language="BGR ", include_default=True, required=False + ) + self.assertEqual(builder.language_systems, {("cyrl", "BGR ")}) + builder.start_feature(location=None, name="test2") + self.assertEqual(builder.language_systems, {("latn", "FRA ")}) def test_language_in_aalt_feature(self): self.assertRaisesRegex( FeatureLibError, - "Language statements are not allowed within \"feature aalt\"", - self.build, "feature aalt { language FRA; } aalt;") + 'Language statements are not allowed within "feature aalt"', + self.build, + "feature aalt { language FRA; } aalt;", + ) def test_language_in_size_feature(self): self.assertRaisesRegex( FeatureLibError, - "Language statements are not allowed within \"feature size\"", - self.build, "feature size { language FRA; } size;") + 'Language statements are not allowed within "feature size"', + self.build, + "feature size { language FRA; } size;", + ) def test_language_in_standalone_lookup(self): self.assertRaisesRegex( FeatureLibError, "Language statements are not allowed within standalone lookup blocks", - self.build, "lookup test { language FRA; } test;") + self.build, + "lookup test { language FRA; } test;", + ) def test_language_required_duplicate(self): self.assertRaisesRegex( @@ -409,13 +474,16 @@ class BuilderTest(unittest.TestCase): " script latn;" " language FRA required;" " substitute [a-z] by [A.sc-Z.sc];" - "} test;") + "} test;", + ) def test_lookup_already_defined(self): self.assertRaisesRegex( FeatureLibError, - "Lookup \"foo\" has already been defined", - self.build, "lookup foo {} foo; lookup foo {} foo;") + 'Lookup "foo" has already been defined', + self.build, + "lookup foo {} foo; lookup foo {} foo;", + ) def test_lookup_multiple_flags(self): self.assertRaisesRegex( @@ -428,7 +496,8 @@ class BuilderTest(unittest.TestCase): " sub f i by f_i;" " lookupflag 2;" " sub f f i by f_f_i;" - "} foo;") + "} foo;", + ) def test_lookup_multiple_types(self): self.assertRaisesRegex( @@ -439,13 +508,16 @@ class BuilderTest(unittest.TestCase): "lookup foo {" " sub f f i by f_f_i;" " sub A from [A.alt1 A.alt2];" - "} foo;") + "} foo;", + ) def test_lookup_inside_feature_aalt(self): self.assertRaisesRegex( FeatureLibError, "Lookup blocks cannot be placed inside 'aalt' features", - self.build, "feature aalt {lookup L {} L;} aalt;") + self.build, + "feature aalt {lookup L {} L;} aalt;", + ) def test_chain_subst_refrences_GPOS_looup(self): self.assertRaisesRegex( @@ -455,7 +527,7 @@ class BuilderTest(unittest.TestCase): "lookup dummy { pos a 50; } dummy;" "feature test {" " sub a' lookup dummy b;" - "} test;" + "} test;", ) def test_chain_pos_refrences_GSUB_looup(self): @@ -466,203 +538,215 @@ class BuilderTest(unittest.TestCase): "lookup dummy { sub a by A; } dummy;" "feature test {" " pos a' lookup dummy b;" - "} test;" + "} test;", ) def test_STAT_elidedfallbackname_already_defined(self): self.assertRaisesRegex( FeatureLibError, - 'ElidedFallbackName is already set.', + "ElidedFallbackName is already set.", self.build, - 'table name {' + "table name {" ' nameid 256 "Roman"; ' - '} name;' - 'table STAT {' + "} name;" + "table STAT {" ' ElidedFallbackName { name "Roman"; };' - ' ElidedFallbackNameID 256;' - '} STAT;') + " ElidedFallbackNameID 256;" + "} STAT;", + ) def test_STAT_elidedfallbackname_set_twice(self): self.assertRaisesRegex( FeatureLibError, - 'ElidedFallbackName is already set.', + "ElidedFallbackName is already set.", self.build, - 'table name {' + "table name {" ' nameid 256 "Roman"; ' - '} name;' - 'table STAT {' + "} name;" + "table STAT {" ' ElidedFallbackName { name "Roman"; };' ' ElidedFallbackName { name "Italic"; };' - '} STAT;') + "} STAT;", + ) def test_STAT_elidedfallbacknameID_already_defined(self): self.assertRaisesRegex( FeatureLibError, - 'ElidedFallbackNameID is already set.', + "ElidedFallbackNameID is already set.", self.build, - 'table name {' + "table name {" ' nameid 256 "Roman"; ' - '} name;' - 'table STAT {' - ' ElidedFallbackNameID 256;' + "} name;" + "table STAT {" + " ElidedFallbackNameID 256;" ' ElidedFallbackName { name "Roman"; };' - '} STAT;') + "} STAT;", + ) def test_STAT_elidedfallbacknameID_not_in_name_table(self): self.assertRaisesRegex( FeatureLibError, - 'ElidedFallbackNameID 256 points to a nameID that does not ' + "ElidedFallbackNameID 256 points to a nameID that does not " 'exist in the "name" table', self.build, - 'table name {' + "table name {" ' nameid 257 "Roman"; ' - '} name;' - 'table STAT {' - ' ElidedFallbackNameID 256;' + "} name;" + "table STAT {" + " ElidedFallbackNameID 256;" ' DesignAxis opsz 1 { name "Optical Size"; };' - '} STAT;') + "} STAT;", + ) def test_STAT_design_axis_name(self): self.assertRaisesRegex( FeatureLibError, 'Expected "name"', self.build, - 'table name {' + "table name {" ' nameid 256 "Roman"; ' - '} name;' - 'table STAT {' + "} name;" + "table STAT {" ' ElidedFallbackName { name "Roman"; };' ' DesignAxis opsz 0 { badtag "Optical Size"; };' - '} STAT;') + "} STAT;", + ) def test_STAT_duplicate_design_axis_name(self): self.assertRaisesRegex( FeatureLibError, 'DesignAxis already defined for tag "opsz".', self.build, - 'table name {' + "table name {" ' nameid 256 "Roman"; ' - '} name;' - 'table STAT {' + "} name;" + "table STAT {" ' ElidedFallbackName { name "Roman"; };' ' DesignAxis opsz 0 { name "Optical Size"; };' ' DesignAxis opsz 1 { name "Optical Size"; };' - '} STAT;') + "} STAT;", + ) def test_STAT_design_axis_duplicate_order(self): self.assertRaisesRegex( FeatureLibError, "DesignAxis already defined for axis number 0.", self.build, - 'table name {' + "table name {" ' nameid 256 "Roman"; ' - '} name;' - 'table STAT {' + "} name;" + "table STAT {" ' ElidedFallbackName { name "Roman"; };' ' DesignAxis opsz 0 { name "Optical Size"; };' ' DesignAxis wdth 0 { name "Width"; };' - ' AxisValue {' - ' location opsz 8;' - ' location wdth 400;' + " AxisValue {" + " location opsz 8;" + " location wdth 400;" ' name "Caption";' - ' };' - '} STAT;') + " };" + "} STAT;", + ) def test_STAT_undefined_tag(self): self.assertRaisesRegex( FeatureLibError, - 'DesignAxis not defined for wdth.', + "DesignAxis not defined for wdth.", self.build, - 'table name {' + "table name {" ' nameid 256 "Roman"; ' - '} name;' - 'table STAT {' + "} name;" + "table STAT {" ' ElidedFallbackName { name "Roman"; };' ' DesignAxis opsz 0 { name "Optical Size"; };' - ' AxisValue { ' - ' location wdth 125; ' + " AxisValue { " + " location wdth 125; " ' name "Wide"; ' - ' };' - '} STAT;') + " };" + "} STAT;", + ) def test_STAT_axis_value_format4(self): self.assertRaisesRegex( FeatureLibError, - 'Axis tag wdth already defined.', + "Axis tag wdth already defined.", self.build, - 'table name {' + "table name {" ' nameid 256 "Roman"; ' - '} name;' - 'table STAT {' + "} name;" + "table STAT {" ' ElidedFallbackName { name "Roman"; };' ' DesignAxis opsz 0 { name "Optical Size"; };' ' DesignAxis wdth 1 { name "Width"; };' ' DesignAxis wght 2 { name "Weight"; };' - ' AxisValue { ' - ' location opsz 8; ' - ' location wdth 125; ' - ' location wdth 125; ' - ' location wght 500; ' + " AxisValue { " + " location opsz 8; " + " location wdth 125; " + " location wdth 125; " + " location wght 500; " ' name "Caption Medium Wide"; ' - ' };' - '} STAT;') + " };" + "} STAT;", + ) def test_STAT_duplicate_axis_value_record(self): # Test for Duplicate AxisValueRecords even when the definition order # is different. self.assertRaisesRegex( FeatureLibError, - 'An AxisValueRecord with these values is already defined.', + "An AxisValueRecord with these values is already defined.", self.build, - 'table name {' + "table name {" ' nameid 256 "Roman"; ' - '} name;' - 'table STAT {' + "} name;" + "table STAT {" ' ElidedFallbackName { name "Roman"; };' ' DesignAxis opsz 0 { name "Optical Size"; };' ' DesignAxis wdth 1 { name "Width"; };' - ' AxisValue {' - ' location opsz 8;' - ' location wdth 400;' + " AxisValue {" + " location opsz 8;" + " location wdth 400;" ' name "Caption";' - ' };' - ' AxisValue {' - ' location wdth 400;' - ' location opsz 8;' + " };" + " AxisValue {" + " location wdth 400;" + " location opsz 8;" ' name "Caption";' - ' };' - '} STAT;') + " };" + "} STAT;", + ) def test_STAT_axis_value_missing_location(self): self.assertRaisesRegex( FeatureLibError, 'Expected "Axis location"', self.build, - 'table name {' + "table name {" ' nameid 256 "Roman"; ' - '} name;' - 'table STAT {' + "} name;" + "table STAT {" ' ElidedFallbackName { name "Roman"; ' - '};' + "};" ' DesignAxis opsz 0 { name "Optical Size"; };' - ' AxisValue { ' + " AxisValue { " ' name "Wide"; ' - ' };' - '} STAT;') + " };" + "} STAT;", + ) def test_STAT_invalid_location_tag(self): self.assertRaisesRegex( FeatureLibError, - 'Tags cannot be longer than 4 characters', + "Tags cannot be longer than 4 characters", self.build, - 'table name {' + "table name {" ' nameid 256 "Roman"; ' - '} name;' - 'table STAT {' + "} name;" + "table STAT {" ' ElidedFallbackName { name "Roman"; ' ' name 3 1 0x0411 "ローマン"; }; ' ' DesignAxis width 0 { name "Width"; };' - '} STAT;') + "} STAT;", + ) def test_extensions(self): class ast_BaseClass(ast.MarkClass): @@ -680,7 +764,9 @@ class BuilderTest(unittest.TestCase): for bcd in self.base.markClass.definitions: if res != "": res += "\n{}".format(indent) - res += "pos base {} {}".format(bcd.glyphs.asFea(), bcd.anchor.asFea()) + res += "pos base {} {}".format( + bcd.glyphs.asFea(), bcd.anchor.asFea() + ) for m in self.marks: res += " mark @{}".format(m.name) res += ";" @@ -693,6 +779,7 @@ class BuilderTest(unittest.TestCase): class testAst(object): MarkBasePosStatement = ast_MarkBasePosStatement + def __getattr__(self, name): return getattr(ast, name) @@ -703,8 +790,9 @@ class BuilderTest(unittest.TestCase): if enumerated: raise FeatureLibError( '"enumerate" is not allowed with ' - 'mark-to-base attachment positioning', - location) + "mark-to-base attachment positioning", + location, + ) base = self.parse_glyphclass_(accept_glyphname=True) if self.next_token_ == "<": marks = self.parse_anchor_marks_() @@ -715,11 +803,10 @@ class BuilderTest(unittest.TestCase): m = self.expect_markClass_reference_() marks.append(m) self.expect_symbol_(";") - return self.ast.MarkBasePosStatement(base, marks, - location=location) + return self.ast.MarkBasePosStatement(base, marks, location=location) def parseBaseClass(self): - if not hasattr(self.doc_, 'baseClasses'): + if not hasattr(self.doc_, "baseClasses"): self.doc_.baseClasses = {} location = self.cur_token_location_ glyphs = self.parse_glyphclass_(accept_glyphname=True) @@ -731,37 +818,39 @@ class BuilderTest(unittest.TestCase): baseClass = ast_BaseClass(name) self.doc_.baseClasses[name] = baseClass self.glyphclasses_.define(name, baseClass) - bcdef = ast_BaseClassDefinition(baseClass, anchor, glyphs, - location=location) + bcdef = ast_BaseClassDefinition( + baseClass, anchor, glyphs, location=location + ) baseClass.addDefinition(bcdef) return bcdef - extensions = { - 'baseClass' : lambda s : s.parseBaseClass() - } + extensions = {"baseClass": lambda s: s.parseBaseClass()} ast = testAst() self.check_fea2fea_file( - "baseClass.feax", base="baseClass.fea", parser=testParser) + "baseClass.feax", base="baseClass.fea", parser=testParser + ) def test_markClass_same_glyph_redefined(self): self.assertRaisesRegex( FeatureLibError, "Glyph acute already defined", self.build, - "markClass [acute] <anchor 350 0> @TOP_MARKS;"*2) + "markClass [acute] <anchor 350 0> @TOP_MARKS;" * 2, + ) def test_markClass_same_glyph_multiple_classes(self): self.assertRaisesRegex( FeatureLibError, - 'Glyph uni0327 cannot be in both @ogonek and @cedilla', + "Glyph uni0327 cannot be in both @ogonek and @cedilla", self.build, "feature mark {" " markClass [uni0327 uni0328] <anchor 0 0> @ogonek;" " pos base [a] <anchor 399 0> mark @ogonek;" " markClass [uni0327] <anchor 0 0> @cedilla;" " pos base [a] <anchor 244 0> mark @cedilla;" - "} mark;") + "} mark;", + ) def test_build_specific_tables(self): features = "feature liga {sub f i by f_i;} liga;" @@ -783,7 +872,7 @@ class BuilderTest(unittest.TestCase): def test_unsupported_subtable_break(self): logger = logging.getLogger("fontTools.otlLib.builder") - with CapturingLogHandler(logger, level='WARNING') as captor: + with CapturingLogHandler(logger, level="WARNING") as captor: self.build( "feature test {" " pos a 10;" @@ -814,10 +903,8 @@ class BuilderTest(unittest.TestCase): FeatureLibError, "Already defined different position for glyph", self.build, - "lookup foo {" - " pos A -45; " - " pos A 45; " - "} foo;") + "lookup foo {" " pos A -45; " " pos A 45; " "} foo;", + ) def test_pairPos_enumRuleOverridenBySinglePair_DEBUG(self): logger = logging.getLogger("fontTools.otlLib.builder") @@ -826,8 +913,56 @@ class BuilderTest(unittest.TestCase): "feature test {" " enum pos A [V Y] -80;" " pos A V -75;" - "} test;") - captor.assertRegex('Already defined position for pair A V at') + "} test;" + ) + captor.assertRegex("Already defined position for pair A V at") + + def test_ignore_empty_lookup_block(self): + # https://github.com/fonttools/fonttools/pull/2277 + font = self.build( + "lookup EMPTY { ; } EMPTY;" "feature ss01 { lookup EMPTY; } ss01;" + ) + assert "GPOS" not in font + assert "GSUB" not in font + + def test_disable_empty_classes(self): + for test in [ + "sub a by c []", + "sub f f [] by f", + "ignore sub a []'", + "ignore sub [] a'", + "sub a []' by b", + "sub [] a' by b", + "rsub [] by a", + "pos [] 120", + "pos a [] 120", + "enum pos a [] 120", + "pos cursive [] <anchor NULL> <anchor NULL>", + "pos base [] <anchor NULL> mark @TOPMARKS", + "pos ligature [] <anchor NULL> mark @TOPMARKS", + "pos mark [] <anchor NULL> mark @TOPMARKS", + "ignore pos a []'", + "ignore pos [] a'", + ]: + self.assertRaisesRegex( + FeatureLibError, + "Empty ", + self.build, + f"markClass a <anchor 150 -10> @TOPMARKS; lookup foo {{ {test}; }} foo;", + ) + self.assertRaisesRegex( + FeatureLibError, + "Empty glyph class in mark class definition", + self.build, + "markClass [] <anchor 150 -10> @TOPMARKS;" + ) + self.assertRaisesRegex( + FeatureLibError, + 'Expected a glyph class with 1 elements after "by", but found a glyph class with 0 elements', + self.build, + "feature test { sub a by []; test};" + ) + def generate_feature_file_test(name): @@ -835,8 +970,7 @@ def generate_feature_file_test(name): for name in BuilderTest.TEST_FEATURE_FILES: - setattr(BuilderTest, "test_FeatureFile_%s" % name, - generate_feature_file_test(name)) + setattr(BuilderTest, "test_FeatureFile_%s" % name, generate_feature_file_test(name)) def generate_fea2fea_file_test(name): @@ -844,8 +978,11 @@ def generate_fea2fea_file_test(name): for name in BuilderTest.TEST_FEATURE_FILES: - setattr(BuilderTest, "test_Fea2feaFile_{}".format(name), - generate_fea2fea_file_test(name)) + setattr( + BuilderTest, + "test_Fea2feaFile_{}".format(name), + generate_fea2fea_file_test(name), + ) if __name__ == "__main__": |