diff --git a/Lib/fontTools/feaLib/ast.py b/Lib/fontTools/feaLib/ast.py index 360e36a85..08cb4cc3c 100644 --- a/Lib/fontTools/feaLib/ast.py +++ b/Lib/fontTools/feaLib/ast.py @@ -14,6 +14,13 @@ class FeatureBlock(object): self.statements = [] +class LookupBlock(object): + def __init__(self, location, name, use_extension): + self.location = location + self.name, self.use_extension = name, use_extension + self.statements = [] + + class GlyphClassDefinition(object): def __init__(self, location, name, glyphs): self.location = location @@ -41,6 +48,11 @@ class IgnoreSubstitutionRule(object): self.prefix, self.glyphs, self.suffix = (prefix, glyphs, suffix) +class LookupReferenceStatement(object): + def __init__(self, location, lookup): + self.location, self.lookup = (location, lookup) + + class ScriptStatement(object): def __init__(self, location, script): self.location = location diff --git a/Lib/fontTools/feaLib/parser.py b/Lib/fontTools/feaLib/parser.py index b4d30a28d..49b54e083 100644 --- a/Lib/fontTools/feaLib/parser.py +++ b/Lib/fontTools/feaLib/parser.py @@ -24,8 +24,11 @@ class Parser(object): def __init__(self, path): self.doc_ = ast.FeatureFile() self.glyphclasses_ = SymbolTable() + self.lookups_ = SymbolTable() self.valuerecords_ = SymbolTable() - self.symbol_tables_ = [self.glyphclasses_, self.valuerecords_] + self.symbol_tables_ = { + self.glyphclasses_, self.lookups_, self.valuerecords_ + } self.next_token_type_, self.next_token_ = (None, None) self.next_token_location_ = None self.lexer_ = IncludingLexer(path) @@ -42,11 +45,13 @@ class Parser(object): self.parse_valuerecord_definition_(vertical=False)) elif self.is_cur_keyword_("languagesystem"): self.parse_languagesystem_() + elif self.is_cur_keyword_("lookup"): + statements.append(self.parse_lookup_(vertical=False)) elif self.is_cur_keyword_("feature"): statements.append(self.parse_feature_block_()) else: - raise ParserError("Expected languagesystem, feature, or " - "glyph class definition", + raise ParserError("Expected feature, languagesystem, " + "lookup, or glyph class definition", self.cur_token_location_) return self.doc_ @@ -147,6 +152,28 @@ class Parser(object): return ast.LanguageStatement(location, language.strip(), include_default, required) + def parse_lookup_(self, vertical): + assert self.is_cur_keyword_("lookup") + location, name = self.cur_token_location_, self.expect_name_() + + if self.next_token_ == ";": + lookup = self.lookups_.resolve(name) + if lookup is None: + raise ParserError("Unknown lookup \"%s\"" % name, + self.cur_token_location_) + self.expect_symbol_(";") + return ast.LookupReferenceStatement(location, lookup) + + use_extension = False + if self.next_token_ == "useExtension": + self.expect_keyword_("useExtension") + use_extension = True + + block = ast.LookupBlock(location, name, use_extension) + self.parse_block_(block, vertical) + self.lookups_.define(name, block) + return block + def parse_script_(self): assert self.is_cur_keyword_("script") location, script = self.cur_token_location_, self.expect_tag_() @@ -232,6 +259,8 @@ class Parser(object): statements.append(self.parse_ignore_()) elif self.is_cur_keyword_("language"): statements.append(self.parse_language_()) + elif self.is_cur_keyword_("lookup"): + statements.append(self.parse_lookup_(vertical)) elif self.is_cur_keyword_("script"): statements.append(self.parse_script_()) elif (self.is_cur_keyword_("substitute") or diff --git a/Lib/fontTools/feaLib/parser_test.py b/Lib/fontTools/feaLib/parser_test.py index 06fddc201..1f39cb347 100644 --- a/Lib/fontTools/feaLib/parser_test.py +++ b/Lib/fontTools/feaLib/parser_test.py @@ -173,6 +173,57 @@ class ParserTest(unittest.TestCase): self.assertTrue(s.include_default) self.assertTrue(s.required) + 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( + ParserError, '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( + ParserError, '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]