diff --git a/Lib/fontTools/feaLib/ast.py b/Lib/fontTools/feaLib/ast.py index aa60aa109..9c4abb0fc 100644 --- a/Lib/fontTools/feaLib/ast.py +++ b/Lib/fontTools/feaLib/ast.py @@ -145,6 +145,20 @@ class MultipleSubstitution(Statement): self.glyph, self.replacement) +class PairAdjustmentPositioning(Statement): + def __init__(self, location, enumerated, + glyphclass1, valuerecord1, glyphclass2, valuerecord2): + Statement.__init__(self, location) + self.enumerated = enumerated + self.glyphclass1, self.valuerecord1 = glyphclass1, valuerecord1 + self.glyphclass2, self.valuerecord2 = glyphclass2, valuerecord2 + + def build(self, builder): + builder.add_pair_pos(self.location, self.enumerated, + self.glyphclass1, self.valuerecord1, + self.glyphclass2, self.valuerecord2) + + class ReverseChainingSingleSubstitution(Statement): def __init__(self, location, old_prefix, old_suffix, mapping): Statement.__init__(self, location) diff --git a/Lib/fontTools/feaLib/builder.py b/Lib/fontTools/feaLib/builder.py index d887f9594..0e2c2aeb4 100644 --- a/Lib/fontTools/feaLib/builder.py +++ b/Lib/fontTools/feaLib/builder.py @@ -304,6 +304,10 @@ class Builder(object): location) lookup.mapping[from_glyph] = to_glyph + def add_pair_pos(self, location, enumerated, + glyph1, value1, glyph2, value2): + pass # TODO: Implement. + def add_single_pos(self, location, glyph, valuerecord): lookup = self.get_lookup_(location, SinglePosBuilder) curValue = lookup.mapping.get(glyph) diff --git a/Lib/fontTools/feaLib/parser.py b/Lib/fontTools/feaLib/parser.py index 5cf67cea9..1424793f0 100644 --- a/Lib/fontTools/feaLib/parser.py +++ b/Lib/fontTools/feaLib/parser.py @@ -60,6 +60,11 @@ class Parser(object): self.anchors_.define(name, anchordef) return anchordef + def parse_enumerate_(self, vertical): + assert self.cur_token_ in {"enumerate", "enum"} + self.advance_lexer_() + return self.parse_position_(enumerated=True, vertical=vertical) + def parse_glyphclass_definition_(self): location, name = self.cur_token_location_, self.cur_token_ self.expect_symbol_("=") @@ -200,13 +205,33 @@ class Parser(object): self.lookups_.define(name, block) return block - def parse_position_(self, vertical): + def is_next_glyphclass_(self): + return (self.next_token_ == "[" or + self.next_token_type_ in (Lexer.GLYPHCLASS, Lexer.NAME)) + + def parse_position_(self, enumerated, vertical): assert self.cur_token_ in {"position", "pos"} location = self.cur_token_location_ - glyphclass = self.parse_glyphclass_(accept_glyphname=True) - valuerec = self.parse_valuerecord_(vertical) + 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) + 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_(";") - return ast.SingleAdjustmentPositioning(location, glyphclass, valuerec) + if gc2 is None: + if enumerated: + raise FeatureLibError( + '"enumerate" is only allowed with pair positionings', + self.cur_token_location_) + return ast.SingleAdjustmentPositioning(location, gc1, value1) + else: + return ast.PairAdjustmentPositioning(location, enumerated, + gc1, value1, gc2, value2) def parse_script_(self): assert self.is_cur_keyword_("script") @@ -357,6 +382,9 @@ class Parser(object): location = self.cur_token_location_ if self.next_token_type_ is Lexer.NAME: name = self.expect_name_() + if name == "NULL": + self.expect_symbol_(">") + return None vrd = self.valuerecords_.resolve(name) if vrd is None: raise FeatureLibError("Unknown valueRecordDef \"%s\"" % name, @@ -442,6 +470,8 @@ class Parser(object): statements.append(self.parse_glyphclass_definition_()) elif self.is_cur_keyword_("anchorDef"): statements.append(self.parse_anchordef_()) + elif self.is_cur_keyword_({"enum", "enumerate"}): + statements.append(self.parse_enumerate_(vertical=vertical)) elif self.is_cur_keyword_("ignore"): statements.append(self.parse_ignore_()) elif self.is_cur_keyword_("language"): @@ -449,7 +479,8 @@ class Parser(object): elif self.is_cur_keyword_("lookup"): statements.append(self.parse_lookup_(vertical)) elif self.is_cur_keyword_({"pos", "position"}): - statements.append(self.parse_position_(vertical)) + statements.append( + self.parse_position_(enumerated=False, vertical=vertical)) elif self.is_cur_keyword_("script"): statements.append(self.parse_script_()) elif (self.is_cur_keyword_({"sub", "substitute", diff --git a/Lib/fontTools/feaLib/parser_test.py b/Lib/fontTools/feaLib/parser_test.py index 8d2d0b17f..5604de4a7 100644 --- a/Lib/fontTools/feaLib/parser_test.py +++ b/Lib/fontTools/feaLib/parser_test.py @@ -256,7 +256,7 @@ class ParserTest(unittest.TestCase): FeatureLibError, 'Unknown lookup "Huh"', self.parse, "feature liga {lookup Huh;} liga;") - def test_gpos_type1_glyph(self): + def test_gpos_type_1_glyph(self): doc = self.parse("feature kern {pos one <1 2 3 4>;} kern;") pos = doc.statements[0].statements[0] self.assertEqual(type(pos), ast.SingleAdjustmentPositioning) @@ -264,20 +264,95 @@ class ParserTest(unittest.TestCase): self.assertEqual(pos.valuerecord.makeString(vertical=False), "<1 2 3 4>") - def test_gpos_type1_glyphclass_horizontal(self): + def test_gpos_type_1_glyphclass_horizontal(self): doc = self.parse("feature kern {pos [one two] -300;} kern;") pos = doc.statements[0].statements[0] self.assertEqual(type(pos), ast.SingleAdjustmentPositioning) self.assertEqual(pos.glyphclass, {"one", "two"}) self.assertEqual(pos.valuerecord.makeString(vertical=False), "-300") - def test_gpos_type1_glyphclass_vertical(self): + def test_gpos_type_1_glyphclass_vertical(self): doc = self.parse("feature vkrn {pos [one two] -300;} vkrn;") pos = doc.statements[0].statements[0] self.assertEqual(type(pos), ast.SingleAdjustmentPositioning) self.assertEqual(pos.glyphclass, {"one", "two"}) self.assertEqual(pos.valuerecord.makeString(vertical=True), "-300") + def test_gpos_type_1_enumerated(self): + self.assertRaisesRegex( + FeatureLibError, + '"enumerate" is only allowed with pair positionings', + self.parse, "feature test {enum pos T 100;} test;") + self.assertRaisesRegex( + FeatureLibError, + '"enumerate" is only allowed with pair positionings', + self.parse, "feature test {enumerate pos T 100;} test;") + + def test_gpos_type_2_format_a(self): + doc = self.parse("feature kern {" + " pos [T V] -60 [a b c] <1 2 3 4>;" + "} kern;") + pos = doc.statements[0].statements[0] + self.assertEqual(type(pos), ast.PairAdjustmentPositioning) + self.assertFalse(pos.enumerated) + self.assertEqual(pos.glyphclass1, {"T", "V"}) + self.assertEqual(pos.valuerecord1.makeString(vertical=False), "-60") + self.assertEqual(pos.glyphclass2, {"a", "b", "c"}) + self.assertEqual(pos.valuerecord2.makeString(vertical=False), + "<1 2 3 4>") + + def test_gpos_type_2_format_a_enumerated(self): + doc = self.parse("feature kern {" + " enum pos [T V] -60 [a b c] <1 2 3 4>;" + "} kern;") + pos = doc.statements[0].statements[0] + self.assertEqual(type(pos), ast.PairAdjustmentPositioning) + self.assertTrue(pos.enumerated) + self.assertEqual(pos.glyphclass1, {"T", "V"}) + self.assertEqual(pos.valuerecord1.makeString(vertical=False), "-60") + self.assertEqual(pos.glyphclass2, {"a", "b", "c"}) + self.assertEqual(pos.valuerecord2.makeString(vertical=False), + "<1 2 3 4>") + + def test_gpos_type_2_format_a_with_null(self): + doc = self.parse("feature kern {" + " pos [T V] <1 2 3 4> [a b c] ;" + "} kern;") + pos = doc.statements[0].statements[0] + self.assertEqual(type(pos), ast.PairAdjustmentPositioning) + self.assertFalse(pos.enumerated) + self.assertEqual(pos.glyphclass1, {"T", "V"}) + self.assertEqual(pos.valuerecord1.makeString(vertical=False), + "<1 2 3 4>") + self.assertEqual(pos.glyphclass2, {"a", "b", "c"}) + self.assertIsNone(pos.valuerecord2) + + def test_gpos_type_2_format_b(self): + doc = self.parse("feature kern {" + " pos [T V] [a b c] <1 2 3 4>;" + "} kern;") + pos = doc.statements[0].statements[0] + self.assertEqual(type(pos), ast.PairAdjustmentPositioning) + self.assertFalse(pos.enumerated) + self.assertEqual(pos.glyphclass1, {"T", "V"}) + self.assertEqual(pos.valuerecord1.makeString(vertical=False), + "<1 2 3 4>") + self.assertEqual(pos.glyphclass2, {"a", "b", "c"}) + self.assertIsNone(pos.valuerecord2) + + def test_gpos_type_2_format_b_enumerated(self): + doc = self.parse("feature kern {" + " enumerate position [T V] [a b c] <1 2 3 4>;" + "} kern;") + pos = doc.statements[0].statements[0] + self.assertEqual(type(pos), ast.PairAdjustmentPositioning) + self.assertTrue(pos.enumerated) + self.assertEqual(pos.glyphclass1, {"T", "V"}) + self.assertEqual(pos.valuerecord1.makeString(vertical=False), + "<1 2 3 4>") + self.assertEqual(pos.glyphclass2, {"a", "b", "c"}) + self.assertIsNone(pos.valuerecord2) + def test_rsub_format_a(self): doc = self.parse("feature test {rsub a [b B] c' d [e E] by C;} test;") rsub = doc.statements[0].statements[0] @@ -494,6 +569,11 @@ class ParserTest(unittest.TestCase): self.assertIsNone(value.xAdvDevice) self.assertEqual(value.yAdvDevice, ((33, -113), (44, -114), (55, 115))) + def test_valuerecord_format_d(self): + doc = self.parse("feature test {valueRecordDef foo;} test;") + value = doc.statements[0].statements[0].value + self.assertIsNone(value) + def test_valuerecord_named(self): doc = self.parse("valueRecordDef <1 2 3 4> foo;" "feature liga {valueRecordDef bar;} liga;")