From cb0adb98b90dcdecfe1754aa9a3538c0f4be33dc Mon Sep 17 00:00:00 2001 From: Sascha Brawer Date: Wed, 9 Dec 2015 22:56:24 +0100 Subject: [PATCH] [feaLib] Parse GPOS type 8: Chaining contextual positioning No output is generated yet, this change is just on the parser. The OpenType Feature File specification is surprisingly vague about the exact syntax of chaining contextual positioning rules, so I expect that we will have to iterate on this parser. However, the test case in parser_test.py gets recognized by `makeotf`, so the current implementation is unlikely to be completely wrong. --- Lib/fontTools/feaLib/ast.py | 11 +++++++++++ Lib/fontTools/feaLib/builder.py | 7 +++++-- Lib/fontTools/feaLib/parser.py | 24 +++++++++++++++++------- Lib/fontTools/feaLib/parser_test.py | 14 ++++++++++++++ 4 files changed, 47 insertions(+), 9 deletions(-) diff --git a/Lib/fontTools/feaLib/ast.py b/Lib/fontTools/feaLib/ast.py index d3e87ff85..b1f0a5f6b 100644 --- a/Lib/fontTools/feaLib/ast.py +++ b/Lib/fontTools/feaLib/ast.py @@ -103,6 +103,17 @@ class AnchorDefinition(Statement): self.name, self.x, self.y, self.contourpoint = name, x, y, contourpoint +class ChainContextPosStatement(Statement): + def __init__(self, location, prefix, glyphs, suffix, lookups): + Statement.__init__(self, location) + self.prefix, self.glyphs, self.suffix = prefix, glyphs, suffix + self.lookups = lookups + + def build(self, builder): + builder.add_chain_context_pos( + self.location, self.prefix, self.glyphs, self.suffix, self.lookups) + + class ChainContextSubstStatement(Statement): def __init__(self, location, old_prefix, old, old_suffix, lookups): Statement.__init__(self, location) diff --git a/Lib/fontTools/feaLib/builder.py b/Lib/fontTools/feaLib/builder.py index ea15a68a1..13ceb08c4 100644 --- a/Lib/fontTools/feaLib/builder.py +++ b/Lib/fontTools/feaLib/builder.py @@ -305,10 +305,13 @@ class Builder(object): lookup_builders.append(None) return lookup_builders + def add_chain_context_pos(self, location, prefix, glyphs, suffix, lookups): + pass # TODO: Implement. + def add_chain_context_subst(self, location, - old_prefix, old, old_suffix, lookups): + prefix, glyphs, suffix, lookups): lookup = self.get_lookup_(location, ChainContextSubstBuilder) - lookup.substitutions.append((old_prefix, old, old_suffix, + lookup.substitutions.append((prefix, glyphs, suffix, self.find_lookup_builders_(lookups))) def add_alternate_subst(self, location, glyph, from_class): diff --git a/Lib/fontTools/feaLib/parser.py b/Lib/fontTools/feaLib/parser.py index 56d3cfd9d..886257159 100644 --- a/Lib/fontTools/feaLib/parser.py +++ b/Lib/fontTools/feaLib/parser.py @@ -177,7 +177,8 @@ class Parser(object): def parse_glyph_pattern_(self): prefix, glyphs, lookups, suffix = ([], [], [], []) - while self.next_token_ not in {"by", "from", ";"}: + while (self.next_token_ not in {"by", "from", ";", "<"} and + self.next_token_type_ != Lexer.NUMBER): gc = self.parse_glyphclass_(accept_glyphname=True) marked = False if self.next_token_ == "'": @@ -293,26 +294,35 @@ class Parser(object): return self.parse_position_mark_(enumerated, vertical) location = self.cur_token_location_ + prefix, glyphs, lookups, suffix = self.parse_glyph_pattern_() gc2, value2 = None, None - gc1 = self.parse_glyphclass_(accept_glyphname=True) - if self.is_next_glyphclass_(): - # Pair positioning, format B: 'pos' gc1 gc2 value1 - gc2 = self.parse_glyphclass_(accept_glyphname=True) + if not prefix and len(glyphs) == 2 and not suffix and not any(lookups): + # Pair positioning, format B: 'pos' glyphs gc2 value1 + gc2 = glyphs[1] + glyphs = [glyphs[0]] + + if prefix or len(glyphs) > 1 or suffix or any(lookups): + # GPOS type 8: Chaining contextual positioning + self.expect_symbol_(";") + return ast.ChainContextPosStatement( + location, prefix, glyphs, suffix, lookups) + value1 = self.parse_valuerecord_(vertical) if self.next_token_ != ";" and gc2 is None: # Pair positioning, format A: 'pos' gc1 value1 gc2 value2 gc2 = self.parse_glyphclass_(accept_glyphname=True) value2 = self.parse_valuerecord_(vertical) self.expect_symbol_(";") + if gc2 is None: if enumerated: raise FeatureLibError( '"enumerate" is only allowed with pair positionings', self.cur_token_location_) - return ast.SinglePosStatement(location, gc1, value1) + return ast.SinglePosStatement(location, glyphs[0], value1) else: return ast.PairPosStatement(location, enumerated, - gc1, value1, gc2, value2) + glyphs[0], value1, gc2, value2) def parse_position_cursive_(self, enumerated, vertical): location = self.cur_token_location_ diff --git a/Lib/fontTools/feaLib/parser_test.py b/Lib/fontTools/feaLib/parser_test.py index 76e492640..4723d2204 100644 --- a/Lib/fontTools/feaLib/parser_test.py +++ b/Lib/fontTools/feaLib/parser_test.py @@ -540,6 +540,20 @@ class ParserTest(unittest.TestCase): " enum pos mark hamza mark @MARK_CLASS_1;" "} test;") + def test_gpos_type_8(self): + doc = self.parse( + "lookup L1 {pos one 100;} L1; lookup L2 {pos two 200;} L2;" + "feature test {" + " pos [A a] [B b] I' lookup L1 [N n]' lookup L2 P' [Y y] [Z z];" + "} test;") + lookup1, lookup2 = doc.statements[0:2] + pos = doc.statements[-1].statements[0] + self.assertEqual(type(pos), ast.ChainContextPosStatement) + self.assertEqual(pos.prefix, [{"A", "a"}, {"B", "b"}]) + self.assertEqual(pos.glyphs, [{"I"}, {"N", "n"}, {"P"}]) + self.assertEqual(pos.suffix, [{"Y", "y"}, {"Z", "z"}]) + self.assertEqual(pos.lookups, [lookup1, lookup2, None]) + def test_markClass(self): doc = self.parse("markClass [acute grave] @MARKS;" "feature test {"