from __future__ import print_function, division, absolute_import from __future__ import unicode_literals from fontTools.feaLib.error import FeatureLibError from fontTools.feaLib.parser import Parser, SymbolTable from fontTools.misc.py23 import * import fontTools.feaLib.ast as ast import codecs import os import shutil import sys 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_anchordef(self): [foo] = self.parse("anchorDef 123 456 foo;").statements self.assertEqual(type(foo), ast.AnchorDefinition) self.assertEqual(foo.name, "foo") self.assertEqual(foo.x, 123) self.assertEqual(foo.y, 456) self.assertEqual(foo.contourpoint, None) def test_anchordef_contourpoint(self): [foo] = self.parse("anchorDef 123 456 contourpoint 5 foo;").statements self.assertEqual(type(foo), ast.AnchorDefinition) self.assertEqual(foo.name, "foo") self.assertEqual(foo.x, 123) self.assertEqual(foo.y, 456) self.assertEqual(foo.contourpoint, 5) def test_feature_block(self): [liga] = self.parse("feature liga {} liga;").statements self.assertEqual(liga.name, "liga") self.assertFalse(liga.use_extension) def test_feature_block_useExtension(self): [liga] = self.parse("feature liga useExtension {} liga;").statements self.assertEqual(liga.name, "liga") self.assertTrue(liga.use_extension) def test_glyphclass(self): [gc] = self.parse("@dash = [endash emdash figuredash];").statements self.assertEqual(gc.name, "dash") self.assertEqual(gc.glyphs, {"endash", "emdash", "figuredash"}) def test_glyphclass_bad(self): self.assertRaisesRegex( FeatureLibError, "Expected glyph name, glyph range, or glyph class reference", self.parse, "@bad = [a 123];") def test_glyphclass_duplicate(self): self.assertRaisesRegex( FeatureLibError, "Glyph class @dup already defined", self.parse, "@dup = [a b]; @dup = [x];") def test_glyphclass_empty(self): [gc] = self.parse("@empty_set = [];").statements self.assertEqual(gc.name, "empty_set") self.assertEqual(gc.glyphs, set()) def test_glyphclass_equality(self): [foo, bar] = self.parse("@foo = [a b]; @bar = @foo;").statements self.assertEqual(foo.glyphs, {"a", "b"}) self.assertEqual(bar.glyphs, {"a", "b"}) def test_glyphclass_range_uppercase(self): [gc] = self.parse("@swashes = [X.swash-Z.swash];").statements self.assertEqual(gc.name, "swashes") self.assertEqual(gc.glyphs, {"X.swash", "Y.swash", "Z.swash"}) def test_glyphclass_range_lowercase(self): [gc] = self.parse("@defg.sc = [d.sc-g.sc];").statements self.assertEqual(gc.name, "defg.sc") self.assertEqual(gc.glyphs, {"d.sc", "e.sc", "f.sc", "g.sc"}) def test_glyphclass_range_digit1(self): [gc] = self.parse("@range = [foo.2-foo.5];").statements self.assertEqual(gc.glyphs, {"foo.2", "foo.3", "foo.4", "foo.5"}) def test_glyphclass_range_digit2(self): [gc] = self.parse("@range = [foo.09-foo.11];").statements self.assertEqual(gc.glyphs, {"foo.09", "foo.10", "foo.11"}) def test_glyphclass_range_digit3(self): [gc] = self.parse("@range = [foo.123-foo.125];").statements self.assertEqual(gc.glyphs, {"foo.123", "foo.124", "foo.125"}) def test_glyphclass_range_bad(self): self.assertRaisesRegex( FeatureLibError, "Bad range: \"a\" and \"foobar\" should have the same length", self.parse, "@bad = [a-foobar];") self.assertRaisesRegex( FeatureLibError, "Bad range: \"A.swash-z.swash\"", self.parse, "@bad = [A.swash-z.swash];") self.assertRaisesRegex( FeatureLibError, "Start of range must be smaller than its end", self.parse, "@bad = [B.swash-A.swash];") self.assertRaisesRegex( FeatureLibError, "Bad range: \"foo.1234-foo.9876\"", self.parse, "@bad = [foo.1234-foo.9876];") def test_glyphclass_range_mixed(self): [gc] = self.parse("@range = [a foo.09-foo.11 X.sc-Z.sc];").statements self.assertEqual(gc.glyphs, { "a", "foo.09", "foo.10", "foo.11", "X.sc", "Y.sc", "Z.sc" }) def test_glyphclass_reference(self): [vowels_lc, vowels_uc, vowels] = self.parse( "@Vowels.lc = [a e i o u]; @Vowels.uc = [A E I O U];" "@Vowels = [@Vowels.lc @Vowels.uc y Y];").statements self.assertEqual(vowels_lc.glyphs, set(list("aeiou"))) self.assertEqual(vowels_uc.glyphs, set(list("AEIOU"))) self.assertEqual(vowels.glyphs, set(list("aeiouyAEIOUY"))) self.assertRaisesRegex( FeatureLibError, "Unknown glyph class @unknown", self.parse, "@bad = [@unknown];") def test_glyphclass_scoping(self): [foo, liga, smcp] = self.parse( "@foo = [a b];" "feature liga { @bar = [@foo l]; } liga;" "feature smcp { @bar = [@foo s]; } smcp;" ).statements self.assertEqual(foo.glyphs, {"a", "b"}) self.assertEqual(liga.statements[0].glyphs, {"a", "b", "l"}) self.assertEqual(smcp.statements[0].glyphs, {"a", "b", "s"}) def test_ignore_sub(self): doc = self.parse("feature test {ignore sub e t' c;} test;") s = doc.statements[0].statements[0] self.assertEqual(type(s), ast.IgnoreSubstitutionRule) self.assertEqual(s.prefix, [{"e"}]) self.assertEqual(s.glyphs, [{"t"}]) self.assertEqual(s.suffix, [{"c"}]) def test_ignore_substitute(self): doc = self.parse( "feature test {" " ignore substitute f [a e] d' [a u]' [e y];" "} test;") s = doc.statements[0].statements[0] self.assertEqual(type(s), ast.IgnoreSubstitutionRule) self.assertEqual(s.prefix, [{"f"}, {"a", "e"}]) self.assertEqual(s.glyphs, [{"d"}, {"a", "u"}]) self.assertEqual(s.suffix, [{"e", "y"}]) def test_language(self): doc = self.parse("feature test {language DEU;} test;") s = doc.statements[0].statements[0] self.assertEqual(type(s), ast.LanguageStatement) self.assertEqual(s.language, "DEU ") self.assertTrue(s.include_default) self.assertFalse(s.required) def test_language_exclude_dflt(self): doc = self.parse("feature test {language DEU exclude_dflt;} test;") s = doc.statements[0].statements[0] self.assertEqual(type(s), ast.LanguageStatement) self.assertEqual(s.language, "DEU ") self.assertFalse(s.include_default) self.assertFalse(s.required) def test_language_exclude_dflt_required(self): doc = self.parse("feature test {" " language DEU exclude_dflt required;" "} test;") s = doc.statements[0].statements[0] self.assertEqual(type(s), ast.LanguageStatement) self.assertEqual(s.language, "DEU ") self.assertFalse(s.include_default) self.assertTrue(s.required) def test_language_include_dflt(self): doc = self.parse("feature test {language DEU include_dflt;} test;") s = doc.statements[0].statements[0] self.assertEqual(type(s), ast.LanguageStatement) self.assertEqual(s.language, "DEU ") self.assertTrue(s.include_default) self.assertFalse(s.required) def test_language_include_dflt_required(self): doc = self.parse("feature test {" " language DEU include_dflt required;" "} test;") s = doc.statements[0].statements[0] self.assertEqual(type(s), ast.LanguageStatement) self.assertEqual(s.language, "DEU ") self.assertTrue(s.include_default) self.assertTrue(s.required) def test_language_DFLT(self): self.assertRaisesRegex( FeatureLibError, '"DFLT" is not a valid language tag; use "dflt" instead', self.parse, "feature test { language DFLT; } test;") def test_lookup_block(self): [lookup] = self.parse("lookup Ligatures {} Ligatures;").statements self.assertEqual(lookup.name, "Ligatures") self.assertFalse(lookup.use_extension) def test_lookup_block_useExtension(self): [lookup] = self.parse("lookup Foo useExtension {} Foo;").statements self.assertEqual(lookup.name, "Foo") self.assertTrue(lookup.use_extension) def test_lookup_block_name_mismatch(self): self.assertRaisesRegex( FeatureLibError, 'Expected "Foo"', self.parse, "lookup Foo {} Bar;") def test_lookup_block_with_horizontal_valueRecordDef(self): doc = self.parse("feature liga {" " lookup look {" " valueRecordDef 123 foo;" " } look;" "} liga;") [liga] = doc.statements [look] = liga.statements [foo] = look.statements self.assertEqual(foo.value.xAdvance, 123) self.assertEqual(foo.value.yAdvance, 0) def test_lookup_block_with_vertical_valueRecordDef(self): doc = self.parse("feature vkrn {" " lookup look {" " valueRecordDef 123 foo;" " } look;" "} vkrn;") [vkrn] = doc.statements [look] = vkrn.statements [foo] = look.statements self.assertEqual(foo.value.xAdvance, 0) self.assertEqual(foo.value.yAdvance, 123) def test_lookup_reference(self): [foo, bar] = self.parse("lookup Foo {} Foo;" "feature Bar {lookup Foo;} Bar;").statements [ref] = bar.statements self.assertEqual(type(ref), ast.LookupReferenceStatement) self.assertEqual(ref.lookup, foo) def test_lookup_reference_unknown(self): self.assertRaisesRegex( FeatureLibError, 'Unknown lookup "Huh"', self.parse, "feature liga {lookup Huh;} liga;") def test_script(self): doc = self.parse("feature test {script cyrl;} test;") s = doc.statements[0].statements[0] self.assertEqual(type(s), ast.ScriptStatement) self.assertEqual(s.script, "cyrl") def test_script_dflt(self): self.assertRaisesRegex( FeatureLibError, '"dflt" is not a valid script tag; use "DFLT" instead', self.parse, "feature test {script dflt;} test;") def test_substitute_single_format_a(self): # GSUB LookupType 1 doc = self.parse("feature smcp {substitute a by a.sc;} smcp;") sub = doc.statements[0].statements[0] self.assertEqual(type(sub), ast.SingleSubstitution) self.assertEqual(sub.mapping, {"a": "a.sc"}) def test_substitute_single_format_b(self): # GSUB LookupType 1 doc = self.parse( "feature smcp {" " substitute [one.fitted one.oldstyle] by one;" "} smcp;") sub = doc.statements[0].statements[0] self.assertEqual(type(sub), ast.SingleSubstitution) self.assertEqual(sub.mapping, { "one.fitted": "one", "one.oldstyle": "one" }) def test_substitute_single_format_c(self): # GSUB LookupType 1 doc = self.parse( "feature smcp {" " substitute [a-d] by [A.sc-D.sc];" "} smcp;") sub = doc.statements[0].statements[0] self.assertEqual(type(sub), ast.SingleSubstitution) self.assertEqual(sub.mapping, { "a": "A.sc", "b": "B.sc", "c": "C.sc", "d": "D.sc" }) def test_substitute_single_format_c_different_num_elements(self): self.assertRaisesRegex( FeatureLibError, 'Expected a glyph class with 4 elements after "by", ' 'but found a glyph class with 26 elements', self.parse, "feature smcp {sub [a-d] by [A.sc-Z.sc];} smcp;") def test_substitute_multiple(self): # GSUB LookupType 2 doc = self.parse("lookup Look {substitute f_f_i by f f i;} Look;") sub = doc.statements[0].statements[0] self.assertEqual(type(sub), ast.MultipleSubstitution) self.assertEqual(sub.glyph, "f_f_i") self.assertEqual(sub.replacement, ("f", "f", "i")) def test_substitute_from(self): # GSUB LookupType 3 doc = self.parse("feature test {" " substitute a from [a.1 a.2 a.3];" "} test;") sub = doc.statements[0].statements[0] self.assertEqual(type(sub), ast.AlternateSubstitution) self.assertEqual(sub.glyph, "a") self.assertEqual(sub.from_class, {"a.1", "a.2", "a.3"}) def test_substitute_from_glyphclass(self): # GSUB LookupType 3 doc = self.parse("feature test {" " @Ampersands = [ampersand.1 ampersand.2];" " substitute ampersand from @Ampersands;" "} test;") [glyphclass, sub] = doc.statements[0].statements self.assertEqual(type(sub), ast.AlternateSubstitution) self.assertEqual(sub.glyph, "ampersand") self.assertEqual(sub.from_class, {"ampersand.1", "ampersand.2"}) def test_substitute_ligature(self): # GSUB LookupType 4 doc = self.parse("feature liga {substitute f f i by f_f_i;} liga;") sub = doc.statements[0].statements[0] self.assertEqual(type(sub), ast.LigatureSubstitution) self.assertEqual(sub.glyphs, [{"f"}, {"f"}, {"i"}]) self.assertEqual(sub.replacement, "f_f_i") def test_substitute_lookups(self): doc = Parser(self.getpath("spec5fi1.fea")).parse() [langsys, ligs, sub, feature] = doc.statements self.assertEqual(feature.statements[0].lookups, [ligs, None, sub]) self.assertEqual(feature.statements[1].lookups, [ligs, None, sub]) def test_substitute_missing_by(self): self.assertRaisesRegex( FeatureLibError, 'Expected "by", "from" or explicit lookup references', self.parse, "feature liga {substitute f f i;} liga;") def test_subtable(self): doc = self.parse("feature test {subtable;} test;") s = doc.statements[0].statements[0] self.assertEqual(type(s), ast.SubtableStatement) def test_valuerecord_format_a_horizontal(self): doc = self.parse("feature liga {valueRecordDef 123 foo;} liga;") value = doc.statements[0].statements[0].value self.assertEqual(value.xPlacement, 0) self.assertEqual(value.yPlacement, 0) self.assertEqual(value.xAdvance, 123) self.assertEqual(value.yAdvance, 0) def test_valuerecord_format_a_vertical(self): doc = self.parse("feature vkrn {valueRecordDef 123 foo;} vkrn;") value = doc.statements[0].statements[0].value self.assertEqual(value.xPlacement, 0) self.assertEqual(value.yPlacement, 0) self.assertEqual(value.xAdvance, 0) self.assertEqual(value.yAdvance, 123) def test_valuerecord_format_b(self): doc = self.parse("feature liga {valueRecordDef <1 2 3 4> foo;} liga;") value = doc.statements[0].statements[0].value self.assertEqual(value.xPlacement, 1) self.assertEqual(value.yPlacement, 2) self.assertEqual(value.xAdvance, 3) self.assertEqual(value.yAdvance, 4) def test_valuerecord_named(self): doc = self.parse("valueRecordDef <1 2 3 4> foo;" "feature liga {valueRecordDef bar;} liga;") value = doc.statements[1].statements[0].value self.assertEqual(value.xPlacement, 1) self.assertEqual(value.yPlacement, 2) self.assertEqual(value.xAdvance, 3) self.assertEqual(value.yAdvance, 4) def test_valuerecord_named_unknown(self): self.assertRaisesRegex( FeatureLibError, "Unknown valueRecordDef \"unknown\"", self.parse, "valueRecordDef foo;") def test_valuerecord_scoping(self): [foo, liga, smcp] = self.parse( "valueRecordDef 789 foo;" "feature liga {valueRecordDef bar;} liga;" "feature smcp {valueRecordDef bar;} smcp;" ).statements self.assertEqual(foo.value.xAdvance, 789) self.assertEqual(liga.statements[0].value.xAdvance, 789) self.assertEqual(smcp.statements[0].value.xAdvance, 789) def test_languagesystem(self): [langsys] = self.parse("languagesystem latn DEU;").statements self.assertEqual(langsys.script, "latn") self.assertEqual(langsys.language, "DEU ") self.assertRaisesRegex( FeatureLibError, 'For script "DFLT", the language must be "dflt"', self.parse, "languagesystem DFLT DEU;") self.assertRaisesRegex( FeatureLibError, '"dflt" is not a valid script tag; use "DFLT" instead', self.parse, "languagesystem dflt dflt;") self.assertRaisesRegex( FeatureLibError, '"DFLT" is not a valid language tag; use "dflt" instead', self.parse, "languagesystem latn DFLT;") self.assertRaisesRegex( FeatureLibError, "Expected ';'", self.parse, "languagesystem latn DEU") self.assertRaisesRegex( FeatureLibError, "longer than 4 characters", self.parse, "languagesystem foobar DEU;") self.assertRaisesRegex( FeatureLibError, "longer than 4 characters", self.parse, "languagesystem latn FOOBAR;") 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.fea" % self.num_tempfiles) with codecs.open(path, "wb", "utf-8") as outfile: outfile.write(text) return Parser(path).parse() @staticmethod def getpath(testfile): path, _ = os.path.split(__file__) return os.path.join(path, "testdata", testfile) class SymbolTableTest(unittest.TestCase): def test_scopes(self): symtab = SymbolTable() symtab.define("foo", 23) self.assertEqual(symtab.resolve("foo"), 23) symtab.enter_scope() self.assertEqual(symtab.resolve("foo"), 23) symtab.define("foo", 42) self.assertEqual(symtab.resolve("foo"), 42) symtab.exit_scope() self.assertEqual(symtab.resolve("foo"), 23) def test_resolve_undefined(self): self.assertEqual(SymbolTable().resolve("abc"), None) if __name__ == "__main__": unittest.main()