| from fontTools.misc.py23 import * |
| from fontTools.misc.loggingTools import CapturingLogHandler |
| from fontTools.feaLib.builder import Builder, addOpenTypeFeatures, \ |
| addOpenTypeFeaturesFromString |
| from fontTools.feaLib.error import FeatureLibError |
| from fontTools.ttLib import TTFont |
| from fontTools.feaLib.parser import Parser |
| from fontTools.feaLib import ast |
| from fontTools.feaLib.lexer import Lexer |
| import difflib |
| import os |
| import shutil |
| import sys |
| import tempfile |
| import logging |
| import unittest |
| |
| |
| def makeTTFont(): |
| glyphs = """ |
| .notdef space slash fraction semicolon period comma ampersand |
| quotedblleft quotedblright quoteleft quoteright |
| zero one two three four five six seven eight nine |
| zero.oldstyle one.oldstyle two.oldstyle three.oldstyle |
| four.oldstyle five.oldstyle six.oldstyle seven.oldstyle |
| eight.oldstyle nine.oldstyle onequarter onehalf threequarters |
| onesuperior twosuperior threesuperior ordfeminine ordmasculine |
| A B C D E F G H I J K L M N O P Q R S T U V W X Y Z |
| a b c d e f g h i j k l m n o p q r s t u v w x y z |
| A.sc B.sc C.sc D.sc E.sc F.sc G.sc H.sc I.sc J.sc K.sc L.sc M.sc |
| N.sc O.sc P.sc Q.sc R.sc S.sc T.sc U.sc V.sc W.sc X.sc Y.sc Z.sc |
| A.alt1 A.alt2 A.alt3 B.alt1 B.alt2 B.alt3 C.alt1 C.alt2 C.alt3 |
| a.alt1 a.alt2 a.alt3 a.end b.alt c.mid d.alt d.mid |
| e.begin e.mid e.end m.begin n.end s.end z.end |
| Eng Eng.alt1 Eng.alt2 Eng.alt3 |
| A.swash B.swash C.swash D.swash E.swash F.swash G.swash H.swash |
| I.swash J.swash K.swash L.swash M.swash N.swash O.swash P.swash |
| Q.swash R.swash S.swash T.swash U.swash V.swash W.swash X.swash |
| Y.swash Z.swash |
| f_l c_h c_k c_s c_t f_f f_f_i f_f_l f_i o_f_f_i s_t f_i.begin |
| a_n_d T_h T_h.swash germandbls ydieresis yacute breve |
| grave acute dieresis macron circumflex cedilla umlaut ogonek caron |
| damma hamza sukun kasratan lam_meem_jeem noon.final noon.initial |
| by feature lookup sub table uni0327 uni0328 e.fina |
| """.split() |
| glyphs.extend("cid{:05d}".format(cid) for cid in range(800, 1001 + 1)) |
| font = TTFont() |
| font.setGlyphOrder(glyphs) |
| return font |
| |
| |
| class BuilderTest(unittest.TestCase): |
| # Feature files in data/*.fea; output gets compared to data/*.ttx. |
| TEST_FEATURE_FILES = """ |
| Attach cid_range enum markClass language_required |
| GlyphClassDef LigatureCaretByIndex LigatureCaretByPos |
| lookup lookupflag feature_aalt ignore_pos |
| GPOS_1 GPOS_1_zero GPOS_2 GPOS_2b GPOS_3 GPOS_4 GPOS_5 GPOS_6 GPOS_8 |
| GSUB_2 GSUB_3 GSUB_6 GSUB_8 |
| spec4h1 spec4h2 spec5d1 spec5d2 spec5fi1 spec5fi2 spec5fi3 spec5fi4 |
| spec5f_ii_1 spec5f_ii_2 spec5f_ii_3 spec5f_ii_4 |
| spec5h1 spec6b_ii spec6d2 spec6e spec6f |
| spec6h_ii spec6h_iii_1 spec6h_iii_3d spec8a spec8b spec8c spec8d |
| 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 |
| name size size2 multiple_feature_blocks omitted_GlyphClassDef |
| ZeroValue_SinglePos_horizontal ZeroValue_SinglePos_vertical |
| ZeroValue_PairPos_horizontal ZeroValue_PairPos_vertical |
| ZeroValue_ChainSinglePos_horizontal ZeroValue_ChainSinglePos_vertical |
| PairPosSubtable ChainSubstSubtable SubstSubtable ChainPosSubtable |
| LigatureSubtable AlternateSubtable MultipleSubstSubtable |
| SingleSubstSubtable aalt_chain_contextual_subst AlternateChained |
| MultipleLookupsPerGlyph MultipleLookupsPerGlyph2 GSUB_6_formats |
| GSUB_5_formats delete_glyph |
| """.split() |
| |
| 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 setUp(self): |
| self.tempdir = None |
| self.num_tempfiles = 0 |
| |
| def tearDown(self): |
| if self.tempdir: |
| shutil.rmtree(self.tempdir) |
| |
| @staticmethod |
| def getpath(testfile): |
| path, _ = os.path.split(__file__) |
| return os.path.join(path, "data", testfile) |
| |
| def temp_path(self, suffix): |
| 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)) |
| |
| 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. |
| if line.startswith("<ttFont "): |
| lines.append("<ttFont>" + os.linesep) |
| else: |
| lines.append(line.rstrip() + os.linesep) |
| 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', 'hhea', 'vhea']) |
| actual = self.read_ttx(path) |
| expected = self.read_ttx(expected_ttx) |
| if replace: |
| for i in range(len(expected)): |
| for k, v in replace.items(): |
| expected[i] = expected[i].replace(k, v) |
| if actual != expected: |
| for line in difflib.unified_diff( |
| expected, actual, fromfile=expected_ttx, tofile=path): |
| sys.stderr.write(line) |
| self.fail("TTX output is different from expected") |
| |
| def build(self, featureFile, tables=None): |
| font = makeTTFont() |
| addOpenTypeFeaturesFromString(font, featureFile, tables=tables) |
| return font |
| |
| def check_feature_file(self, name): |
| font = makeTTFont() |
| feapath = self.getpath("%s.fea" % name) |
| addOpenTypeFeatures(font, feapath) |
| self.expect_ttx(font, self.getpath("%s.ttx" % name)) |
| # Make sure we can produce binary OpenType tables, not just XML. |
| for tag in ('GDEF', 'GSUB', 'GPOS'): |
| if tag in font: |
| font[tag].compile(font) |
| 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}) |
| |
| def check_fea2fea_file(self, name, base=None, parser=Parser): |
| font = makeTTFont() |
| 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")) |
| with open(self.getpath(base or fname), "r", encoding="utf-8") as ofile: |
| expected = self.normal_fea(ofile.readlines()) |
| |
| 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))) |
| |
| def normal_fea(self, lines): |
| output = [] |
| skip = 0 |
| for l in lines: |
| l = l.strip() |
| if l.startswith("#test-fea2fea:"): |
| if len(l) > 15: |
| output.append(l[15:].strip()) |
| skip = 1 |
| x = l.find("#") |
| if x >= 0: |
| l = l[:x].strip() |
| if not len(l): |
| continue |
| if skip > 0: |
| skip = skip - 1 |
| continue |
| output.append(l) |
| return output |
| |
| def test_alternateSubst_multipleSubstitutionsForSameGlyph(self): |
| self.assertRaisesRegex( |
| FeatureLibError, |
| "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;") |
| |
| def test_singleSubst_multipleIdenticalSubstitutionsForSameGlyph_info(self): |
| logger = logging.getLogger("fontTools.feaLib.builder") |
| with CapturingLogHandler(logger, "INFO") as captor: |
| self.build( |
| "feature test {" |
| " 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"') |
| |
| def test_multipleSubst_multipleSubstitutionsForSameGlyph(self): |
| self.assertRaisesRegex( |
| FeatureLibError, |
| "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;") |
| |
| def test_multipleSubst_multipleIdenticalSubstitutionsForSameGlyph_info(self): |
| logger = logging.getLogger("fontTools.feaLib.builder") |
| with CapturingLogHandler(logger, "INFO") as captor: |
| 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;") |
| 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 |
| logger = logging.getLogger("fontTools.otlLib.builder") |
| with CapturingLogHandler(logger, "DEBUG") as captor: |
| # the pair "yacute semicolon" is redefined in the enum pos |
| font = self.build( |
| "@Y_LC = [y yacute ydieresis];" |
| "@SMALL_PUNC = [comma semicolon period];" |
| "feature kern {" |
| " pos yacute semicolon -70;" |
| " enum pos @Y_LC semicolon -80;" |
| " pos @Y_LC @SMALL_PUNC -100;" |
| "} 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}) |
| |
| def test_singleSubst_multipleSubstitutionsForSameGlyph(self): |
| self.assertRaisesRegex( |
| FeatureLibError, |
| 'Already defined rule for replacing glyph "e" by "E.sc"', |
| self.build, |
| "feature test {" |
| " sub [a-z] by [A.sc-Z.sc];" |
| " sub e by e.fina;" |
| "} 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;") |
| |
| def test_feature_outside_aalt(self): |
| self.assertRaisesRegex( |
| FeatureLibError, |
| 'Feature references are only allowed inside "feature aalt"', |
| 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;") |
| |
| def test_GlyphClassDef_conflictingClasses(self): |
| self.assertRaisesRegex( |
| FeatureLibError, "Glyph X was assigned to a different class", |
| self.build, |
| "table GDEF {" |
| " GlyphClassDef [a b], [X], , ;" |
| " GlyphClassDef [a b X], , , ;" |
| "} 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')}) |
| |
| def test_languagesystem_duplicate(self): |
| self.assertRaisesRegex( |
| FeatureLibError, |
| '"languagesystem cyrl RUS" has already been specified', |
| 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')}) |
| |
| def test_languagesystem_DFLT_dflt_not_first(self): |
| self.assertRaisesRegex( |
| FeatureLibError, |
| "If \"languagesystem DFLT dflt\" is present, " |
| "it must be the first of the languagesystem statements", |
| 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 " |
| "precede all other languagesystems", |
| self.build, |
| "languagesystem DFLT dflt; " |
| "languagesystem latn dflt; " |
| "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')}) |
| |
| 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;") |
| |
| 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;") |
| |
| 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;") |
| |
| 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 ')}) |
| |
| 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;") |
| |
| 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;") |
| |
| 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;") |
| |
| def test_language_required_duplicate(self): |
| self.assertRaisesRegex( |
| FeatureLibError, |
| r"Language FRA \(script latn\) has already specified " |
| "feature scmp as its required feature", |
| self.build, |
| "feature scmp {" |
| " script latn;" |
| " language FRA required;" |
| " language DEU required;" |
| " substitute [a-z] by [A.sc-Z.sc];" |
| "} scmp;" |
| "feature test {" |
| " script latn;" |
| " language FRA required;" |
| " substitute [a-z] by [A.sc-Z.sc];" |
| "} test;") |
| |
| def test_lookup_already_defined(self): |
| self.assertRaisesRegex( |
| FeatureLibError, |
| "Lookup \"foo\" has already been defined", |
| self.build, "lookup foo {} foo; lookup foo {} foo;") |
| |
| def test_lookup_multiple_flags(self): |
| self.assertRaisesRegex( |
| FeatureLibError, |
| "Within a named lookup block, all rules must be " |
| "of the same lookup type and flag", |
| self.build, |
| "lookup foo {" |
| " lookupflag 1;" |
| " sub f i by f_i;" |
| " lookupflag 2;" |
| " sub f f i by f_f_i;" |
| "} foo;") |
| |
| def test_lookup_multiple_types(self): |
| self.assertRaisesRegex( |
| FeatureLibError, |
| "Within a named lookup block, all rules must be " |
| "of the same lookup type and flag", |
| self.build, |
| "lookup foo {" |
| " sub f f i by f_f_i;" |
| " sub A from [A.alt1 A.alt2];" |
| "} 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;") |
| |
| def test_chain_subst_refrences_GPOS_looup(self): |
| self.assertRaisesRegex( |
| FeatureLibError, |
| "Missing index of the specified lookup, might be a positioning lookup", |
| self.build, |
| "lookup dummy { pos a 50; } dummy;" |
| "feature test {" |
| " sub a' lookup dummy b;" |
| "} test;" |
| ) |
| |
| def test_chain_pos_refrences_GSUB_looup(self): |
| self.assertRaisesRegex( |
| FeatureLibError, |
| "Missing index of the specified lookup, might be a substitution lookup", |
| self.build, |
| "lookup dummy { sub a by A; } dummy;" |
| "feature test {" |
| " pos a' lookup dummy b;" |
| "} test;" |
| ) |
| |
| def test_extensions(self): |
| class ast_BaseClass(ast.MarkClass): |
| def asFea(self, indent=""): |
| return "" |
| |
| class ast_BaseClassDefinition(ast.MarkClassDefinition): |
| def asFea(self, indent=""): |
| return "" |
| |
| class ast_MarkBasePosStatement(ast.MarkBasePosStatement): |
| def asFea(self, indent=""): |
| if isinstance(self.base, ast.MarkClassName): |
| res = "" |
| for bcd in self.base.markClass.definitions: |
| if res != "": |
| res += "\n{}".format(indent) |
| res += "pos base {} {}".format(bcd.glyphs.asFea(), bcd.anchor.asFea()) |
| for m in self.marks: |
| res += " mark @{}".format(m.name) |
| res += ";" |
| else: |
| res = "pos base {}".format(self.base.asFea()) |
| for a, m in self.marks: |
| res += " {} mark @{}".format(a.asFea(), m.name) |
| res += ";" |
| return res |
| |
| class testAst(object): |
| MarkBasePosStatement = ast_MarkBasePosStatement |
| def __getattr__(self, name): |
| return getattr(ast, name) |
| |
| class testParser(Parser): |
| def parse_position_base_(self, enumerated, vertical): |
| location = self.cur_token_location_ |
| self.expect_keyword_("base") |
| if enumerated: |
| raise FeatureLibError( |
| '"enumerate" is not allowed with ' |
| 'mark-to-base attachment positioning', |
| location) |
| base = self.parse_glyphclass_(accept_glyphname=True) |
| if self.next_token_ == "<": |
| marks = self.parse_anchor_marks_() |
| else: |
| marks = [] |
| while self.next_token_ == "mark": |
| self.expect_keyword_("mark") |
| m = self.expect_markClass_reference_() |
| marks.append(m) |
| self.expect_symbol_(";") |
| return self.ast.MarkBasePosStatement(base, marks, |
| location=location) |
| |
| def parseBaseClass(self): |
| if not hasattr(self.doc_, 'baseClasses'): |
| self.doc_.baseClasses = {} |
| location = self.cur_token_location_ |
| glyphs = self.parse_glyphclass_(accept_glyphname=True) |
| anchor = self.parse_anchor_() |
| name = self.expect_class_name_() |
| self.expect_symbol_(";") |
| baseClass = self.doc_.baseClasses.get(name) |
| if baseClass is None: |
| baseClass = ast_BaseClass(name) |
| self.doc_.baseClasses[name] = baseClass |
| self.glyphclasses_.define(name, baseClass) |
| bcdef = ast_BaseClassDefinition(baseClass, anchor, glyphs, |
| location=location) |
| baseClass.addDefinition(bcdef) |
| return bcdef |
| |
| extensions = { |
| 'baseClass' : lambda s : s.parseBaseClass() |
| } |
| ast = testAst() |
| |
| self.check_fea2fea_file( |
| "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) |
| |
| def test_markClass_same_glyph_multiple_classes(self): |
| self.assertRaisesRegex( |
| FeatureLibError, |
| '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;") |
| |
| def test_build_specific_tables(self): |
| features = "feature liga {sub f i by f_i;} liga;" |
| font = self.build(features) |
| assert "GSUB" in font |
| |
| font2 = self.build(features, tables=set()) |
| assert "GSUB" not in font2 |
| |
| def test_build_unsupported_tables(self): |
| self.assertRaises(NotImplementedError, self.build, "", tables={"FOO"}) |
| |
| def test_build_pre_parsed_ast_featurefile(self): |
| f = UnicodeIO("feature liga {sub f i by f_i;} liga;") |
| tree = Parser(f).parse() |
| font = makeTTFont() |
| addOpenTypeFeatures(font, tree) |
| assert "GSUB" in font |
| |
| def test_unsupported_subtable_break(self): |
| logger = logging.getLogger("fontTools.otlLib.builder") |
| with CapturingLogHandler(logger, level='WARNING') as captor: |
| self.build( |
| "feature test {" |
| " pos a 10;" |
| " subtable;" |
| " pos b 10;" |
| "} test;" |
| ) |
| |
| captor.assertRegex( |
| '<features>:1:32: unsupported "subtable" statement for lookup type' |
| ) |
| |
| def test_skip_featureNames_if_no_name_table(self): |
| features = ( |
| "feature ss01 {" |
| " featureNames {" |
| ' name "ignored as we request to skip name table";' |
| " };" |
| " sub A by A.alt1;" |
| "} ss01;" |
| ) |
| font = self.build(features, tables=["GSUB"]) |
| self.assertIn("GSUB", font) |
| self.assertNotIn("name", font) |
| |
| def test_singlePos_multiplePositionsForSameGlyph(self): |
| self.assertRaisesRegex( |
| FeatureLibError, |
| "Already defined different position for glyph", |
| self.build, |
| "lookup foo {" |
| " pos A -45; " |
| " pos A 45; " |
| "} foo;") |
| |
| def test_pairPos_enumRuleOverridenBySinglePair_DEBUG(self): |
| logger = logging.getLogger("fontTools.otlLib.builder") |
| with CapturingLogHandler(logger, "DEBUG") as captor: |
| self.build( |
| "feature test {" |
| " enum pos A [V Y] -80;" |
| " pos A V -75;" |
| "} test;") |
| captor.assertRegex('Already defined position for pair A V at') |
| |
| |
| def generate_feature_file_test(name): |
| return lambda self: self.check_feature_file(name) |
| |
| |
| for name in BuilderTest.TEST_FEATURE_FILES: |
| setattr(BuilderTest, "test_FeatureFile_%s" % name, |
| generate_feature_file_test(name)) |
| |
| |
| def generate_fea2fea_file_test(name): |
| return lambda self: self.check_fea2fea_file(name) |
| |
| |
| for name in BuilderTest.TEST_FEATURE_FILES: |
| setattr(BuilderTest, "test_Fea2feaFile_{}".format(name), |
| generate_fea2fea_file_test(name)) |
| |
| |
| if __name__ == "__main__": |
| sys.exit(unittest.main()) |