From 944fab8a5fb67c12ffe83eae257da8c81ffb284d Mon Sep 17 00:00:00 2001 From: Sascha Brawer Date: Sat, 1 Aug 2015 19:58:54 +0200 Subject: [PATCH] [feaLib] Implement parsing of feature blocks --- Lib/fontTools/feaLib/ast.py | 14 ++++++++++++ Lib/fontTools/feaLib/parser.py | 33 ++++++++++++++++++++++++++--- Lib/fontTools/feaLib/parser_test.py | 18 +++++++++++++++- 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/Lib/fontTools/feaLib/ast.py b/Lib/fontTools/feaLib/ast.py index 7c268119d..133bdce0b 100644 --- a/Lib/fontTools/feaLib/ast.py +++ b/Lib/fontTools/feaLib/ast.py @@ -15,6 +15,20 @@ class FeatureFile(object): s.write(out, linesep) +class FeatureBlock(object): + def __init__(self, location, tag): + self.location = location + self.tag = tag + self.statements = [] + + def write(self, out, linesep): + tag = self.tag.strip() + write(out, "feature %s {%s" % (tag, linesep)) + for s in self.statements: + s.write(out, linesep) + write(out, "} %s;%s" % (tag, linesep)) + + class GlyphClassDefinition(object): def __init__(self, location, name, glyphs): self.location = location diff --git a/Lib/fontTools/feaLib/parser.py b/Lib/fontTools/feaLib/parser.py index 9cc4d1dd2..4f480c114 100644 --- a/Lib/fontTools/feaLib/parser.py +++ b/Lib/fontTools/feaLib/parser.py @@ -34,11 +34,12 @@ class Parser(object): while self.next_token_type_ is not None: self.advance_lexer_() if self.cur_token_type_ is Lexer.GLYPHCLASS: - self.parse_glyphclass_definition_() + glyphclass = self.parse_glyphclass_definition_() + self.doc_.statements.append(glyphclass) elif self.is_cur_keyword_("languagesystem"): self.parse_languagesystem_() elif self.is_cur_keyword_("feature"): - break # TODO: Implement + self.parse_feature_block_() else: raise ParserError("Expected languagesystem, feature, or " "glyph class definition", @@ -62,7 +63,7 @@ class Parser(object): location) glyphclass = ast.GlyphClassDefinition(location, name, glyphs) self.glyphclasses_[-1][name] = glyphclass - self.doc_.statements.append(glyphclass) + return glyphclass def parse_glyphclass_reference_(self): result = set() @@ -103,12 +104,38 @@ class Parser(object): return result def parse_languagesystem_(self): + assert self.cur_token_ == "languagesystem" location = self.cur_token_location_ script, language = self.expect_tag_(), self.expect_tag_() self.expect_symbol_(";") langsys = ast.LanguageSystemStatement(location, script, language) self.doc_.statements.append(langsys) + def parse_feature_block_(self): + assert self.cur_token_ == "feature" + location = self.cur_token_location_ + tag = self.expect_tag_() + self.expect_symbol_("{") + self.glyphclasses_.append({}) + block = ast.FeatureBlock(location, tag) + self.doc_.statements.append(block) + + while self.next_token_ != "}": + self.advance_lexer_() + if self.cur_token_type_ is Lexer.GLYPHCLASS: + block.statements.append(self.parse_glyphclass_definition_()) + else: + raise ParserError("Expected glyph class definition", + self.cur_token_location_) + + self.expect_symbol_("}") + self.glyphclasses_.pop() + endtag = self.expect_tag_() + if tag != endtag: + raise ParserError("Expected \"%s\"" % tag.strip(), + self.cur_token_location_) + self.expect_symbol_(";") + def is_cur_keyword_(self, k): return (self.cur_token_type_ is Lexer.NAME) and (self.cur_token_ == k) diff --git a/Lib/fontTools/feaLib/parser_test.py b/Lib/fontTools/feaLib/parser_test.py index 80187215a..b8d214c05 100644 --- a/Lib/fontTools/feaLib/parser_test.py +++ b/Lib/fontTools/feaLib/parser_test.py @@ -89,7 +89,7 @@ class ParserTest(unittest.TestCase): 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.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"))) @@ -98,6 +98,16 @@ class ParserTest(unittest.TestCase): ParserError, "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_languagesystem(self): [langsys] = self.parse("languagesystem latn DEU;").statements self.assertEqual(langsys.script, "latn") @@ -112,6 +122,12 @@ class ParserTest(unittest.TestCase): ParserError, "longer than 4 characters", self.parse, "languagesystem latn FOOBAR") + def test_feature_block(self): + [liga] = self.parse("feature liga {} liga;").statements + self.assertEqual(liga.tag, "liga") + + # TODO: Implement the needed bits in the parser. + @unittest.expectedFailure def test_roundtrip(self): self.roundtrip("mini.fea")