diff --git a/Lib/fontTools/feaLib/ast.py b/Lib/fontTools/feaLib/ast.py index d49f9e24f..42e8f59ea 100644 --- a/Lib/fontTools/feaLib/ast.py +++ b/Lib/fontTools/feaLib/ast.py @@ -475,13 +475,15 @@ class ScriptStatement(Statement): class SinglePosStatement(Statement): - def __init__(self, location, glyphs, valuerecord): + def __init__(self, location, pos, prefix, suffix): Statement.__init__(self, location) - self.glyphs, self.valuerecord = glyphs, valuerecord + self.pos, self.prefix, self.suffix = pos, prefix, suffix def build(self, builder): - for glyph in self.glyphs.glyphSet(): - builder.add_single_pos(self.location, glyph, self.valuerecord) + pos = {} + for glyphs, value in self.pos: + pos.update({glyph: value for glyph in glyphs.glyphSet()}) + builder.add_single_pos(self.location, self.prefix, self.suffix, pos) class SubtableStatement(Statement): diff --git a/Lib/fontTools/feaLib/builder.py b/Lib/fontTools/feaLib/builder.py index be6c7f468..c6cfd511b 100644 --- a/Lib/fontTools/feaLib/builder.py +++ b/Lib/fontTools/feaLib/builder.py @@ -620,9 +620,14 @@ class Builder(object): lookup = self.get_lookup_(location, SpecificPairPosBuilder) lookup.add_pair(location, glyph1, value1, glyph2, value2) - def add_single_pos(self, location, glyph, valuerecord): + def add_single_pos(self, location, prefix, suffix, mapping): + if prefix or suffix: + # TODO: https://github.com/behdad/fonttools/issues/485 + raise FeatureLibError("Contextual SinglePos not yet implemented", + self.location) lookup = self.get_lookup_(location, SinglePosBuilder) - lookup.add_pos(location, glyph, valuerecord) + for glyph, value in mapping.items(): + lookup.add_pos(location, glyph, value) def setGlyphClass_(self, location, glyph, glyphClass): oldClass, oldLocation = self.glyphClassDefs_.get(glyph, (None, None)) diff --git a/Lib/fontTools/feaLib/parser.py b/Lib/fontTools/feaLib/parser.py index 5ab995319..5d22ebe67 100644 --- a/Lib/fontTools/feaLib/parser.py +++ b/Lib/fontTools/feaLib/parser.py @@ -243,10 +243,9 @@ class Parser(object): else: return ast.GlyphClassName(self.cur_token_location_, gc) - def parse_glyph_pattern_(self): - prefix, glyphs, lookups, suffix = ([], [], [], []) - while (self.next_token_ not in {"by", "from", ";", "<"} and - self.next_token_type_ != Lexer.NUMBER): + def parse_glyph_pattern_(self, vertical): + prefix, glyphs, lookups, values, suffix = ([], [], [], [], []) + while self.next_token_ not in {"by", "from", ";"}: gc = self.parse_glyphclass_(accept_glyphname=True) marked = False if self.next_token_ == "'": @@ -259,6 +258,11 @@ class Parser(object): else: prefix.append(gc) + if self.is_next_value_(): + values.append(self.parse_valuerecord_(vertical)) + else: + values.append(None) + lookup = None if self.next_token_ == "lookup": self.expect_keyword_("lookup") @@ -277,17 +281,22 @@ class Parser(object): if not glyphs and not suffix: # eg., "sub f f i by" assert lookups == [] - return ([], prefix, [None] * len(prefix), []) + return ([], prefix, [None] * len(prefix), values, []) else: - return (prefix, glyphs, lookups, suffix) + return (prefix, glyphs, lookups, values, suffix) def parse_ignore_(self): assert self.is_cur_keyword_("ignore") location = self.cur_token_location_ self.advance_lexer_() if self.cur_token_ in ["substitute", "sub"]: - prefix, glyphs, lookups, suffix = self.parse_glyph_pattern_() + prefix, glyphs, lookups, values, suffix = \ + self.parse_glyph_pattern_(vertical=False) self.expect_symbol_(";") + if any(lookups): + raise FeatureLibError( + "No lookups can be specified for \"ignore sub\"", + location) return ast.IgnoreSubstitutionRule(location, prefix, glyphs, suffix) raise FeatureLibError( "Expected \"substitute\"", self.next_token_location_) @@ -415,35 +424,33 @@ 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 - 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]] + prefix, glyphs, lookups, values, suffix = \ + self.parse_glyph_pattern_(vertical) + self.expect_symbol_(";") - if prefix or len(glyphs) > 1 or suffix or any(lookups): - # GPOS type 8: Chaining contextual positioning - self.expect_symbol_(";") + if any(lookups): + # GPOS type 8: Chaining contextual positioning; explicit lookups + if any(values): + raise FeatureLibError( + "If \"lookup\" is present, no values must be specified", + location) 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_(";") + # Pair positioning, format A: "pos V 10 A -10;" + # Pair positioning, format B: "pos V A -20;" + if not prefix and not suffix and len(glyphs) == 2: + if values[0] is None: # Format B: "pos V A -20;" + values.reverse() + return ast.PairPosStatement( + location, enumerated, + glyphs[0], values[0], glyphs[1], values[1]) - if gc2 is None: - if enumerated: - raise FeatureLibError( - '"enumerate" is only allowed with pair positionings', - self.cur_token_location_) - return ast.SinglePosStatement(location, glyphs[0], value1) - else: - return ast.PairPosStatement(location, enumerated, - glyphs[0], value1, gc2, value2) + if enumerated: + raise FeatureLibError( + '"enumerate" is only allowed with pair positionings', location) + return ast.SinglePosStatement(location, zip(glyphs, values), + prefix, suffix) def parse_position_cursive_(self, enumerated, vertical): location = self.cur_token_location_ @@ -512,8 +519,11 @@ class Parser(object): assert self.cur_token_ in {"substitute", "sub", "reversesub", "rsub"} location = self.cur_token_location_ reverse = self.cur_token_ in {"reversesub", "rsub"} - old_prefix, old, lookups, old_suffix = self.parse_glyph_pattern_() - + old_prefix, old, lookups, values, old_suffix = \ + self.parse_glyph_pattern_(vertical=False) + if any(values): + raise FeatureLibError( + "Substitution statements cannot contain values", location) new = [] if self.next_token_ == "by": keyword = self.expect_keyword_("by") @@ -696,6 +706,9 @@ class Parser(object): self.expect_symbol_(">") return result + def is_next_value_(self): + return self.next_token_type_ is Lexer.NUMBER or self.next_token_ == "<" + def parse_valuerecord_(self, vertical): if self.next_token_type_ is Lexer.NUMBER: number, location = self.expect_number_(), self.cur_token_location_ diff --git a/Lib/fontTools/feaLib/parser_test.py b/Lib/fontTools/feaLib/parser_test.py index edfca9ce7..ab0af8a17 100644 --- a/Lib/fontTools/feaLib/parser_test.py +++ b/Lib/fontTools/feaLib/parser_test.py @@ -485,23 +485,37 @@ class ParserTest(unittest.TestCase): doc = self.parse("feature kern {pos one <1 2 3 4>;} kern;") pos = doc.statements[0].statements[0] self.assertIsInstance(pos, ast.SinglePosStatement) - self.assertEqual(glyphstr([pos.glyphs]), "one") - self.assertEqual(pos.valuerecord.makeString(vertical=False), - "<1 2 3 4>") + [(glyphs, value)] = pos.pos + self.assertEqual(glyphstr([glyphs]), "one") + self.assertEqual(value.makeString(vertical=False), "<1 2 3 4>") 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.assertIsInstance(pos, ast.SinglePosStatement) - self.assertEqual(glyphstr([pos.glyphs]), "[one two]") - self.assertEqual(pos.valuerecord.makeString(vertical=False), "-300") + [(glyphs, value)] = pos.pos + self.assertEqual(glyphstr([glyphs]), "[one two]") + self.assertEqual(value.makeString(vertical=False), "-300") 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.assertIsInstance(pos, ast.SinglePosStatement) - self.assertEqual(glyphstr([pos.glyphs]), "[one two]") - self.assertEqual(pos.valuerecord.makeString(vertical=True), "-300") + [(glyphs, value)] = pos.pos + self.assertEqual(glyphstr([glyphs]), "[one two]") + self.assertEqual(value.makeString(vertical=True), "-300") + + def test_gpos_type_1_multiple(self): + doc = self.parse("feature f {pos one'1 two'2 [five six]'56;} f;") + pos = doc.statements[0].statements[0] + self.assertIsInstance(pos, ast.SinglePosStatement) + [(glyphs1, val1), (glyphs2, val2), (glyphs3, val3)] = pos.pos + self.assertEqual(glyphstr([glyphs1]), "one") + self.assertEqual(val1.makeString(vertical=False), "1") + self.assertEqual(glyphstr([glyphs2]), "two") + self.assertEqual(val2.makeString(vertical=False), "2") + self.assertEqual(glyphstr([glyphs3]), "[five six]") + self.assertEqual(val3.makeString(vertical=False), "56") def test_gpos_type_1_enumerated(self): self.assertRaisesRegex( @@ -724,6 +738,16 @@ class ParserTest(unittest.TestCase): self.assertEqual(glyphstr(pos.suffix), "[Y y] [Z z]") self.assertEqual(pos.lookups, [lookup1, lookup2, None]) + def test_gpos_type_8_lookup_with_values(self): + self.assertRaisesRegex( + FeatureLibError, + 'If "lookup" is present, no values must be specified', + self.parse, + "lookup L1 {pos one 100;} L1;" + "feature test {" + " pos A' lookup L1 B' 20;" + "} test;") + def test_markClass(self): doc = self.parse("markClass [acute grave] @MARKS;") mc = doc.statements[0] @@ -902,6 +926,12 @@ class ParserTest(unittest.TestCase): 'but found a glyph class with 26 elements', self.parse, "feature smcp {sub [a-d] by [A.sc-Z.sc];} smcp;") + def test_sub_with_values(self): + self.assertRaisesRegex( + FeatureLibError, + "Substitution statements cannot contain values", + self.parse, "feature smcp {sub A' 20 by A.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]