From 00714511f0ca39f6e7c92a4e416a65d01b331c52 Mon Sep 17 00:00:00 2001 From: Sascha Brawer Date: Tue, 11 Aug 2015 12:22:07 +0200 Subject: [PATCH] [feaLib] Parse chained contextual substitution rules --- Lib/fontTools/feaLib/ast.py | 1 + Lib/fontTools/feaLib/parser.py | 39 ++++++++++++++++++----- Lib/fontTools/feaLib/parser_test.py | 12 ++++++- Lib/fontTools/feaLib/testdata/spec5fi.fea | 18 +++++++++++ 4 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 Lib/fontTools/feaLib/testdata/spec5fi.fea diff --git a/Lib/fontTools/feaLib/ast.py b/Lib/fontTools/feaLib/ast.py index 08cb4cc3c..175cd61e7 100644 --- a/Lib/fontTools/feaLib/ast.py +++ b/Lib/fontTools/feaLib/ast.py @@ -64,6 +64,7 @@ class SubstitutionRule(object): self.location, self.old, self.new = (location, old, new) self.old_prefix = [] self.old_suffix = [] + self.lookups = [None] * len(old) class ValueRecord(object): diff --git a/Lib/fontTools/feaLib/parser.py b/Lib/fontTools/feaLib/parser.py index 49b54e083..bea9b452c 100644 --- a/Lib/fontTools/feaLib/parser.py +++ b/Lib/fontTools/feaLib/parser.py @@ -111,7 +111,7 @@ class Parser(object): return result def parse_glyph_pattern_(self): - prefix, glyphs, suffix = ([], [], []) + prefix, glyphs, lookups, suffix = ([], [], [], []) while self.next_token_ not in {"by", ";"}: gc = self.parse_glyphclass_(accept_glyphname=True) marked = False @@ -124,17 +124,33 @@ class Parser(object): suffix.append(gc) else: prefix.append(gc) + + lookup = None + if self.next_token_ == "lookup": + self.expect_keyword_("lookup") + if not marked: + raise ParserError("Lookups can only follow marked glyphs", + self.cur_token_location_) + lookup_name = self.expect_name_() + lookup = self.lookups_.resolve(lookup_name) + if lookup is None: + raise ParserError('Unknown lookup "%s"' % lookup_name, + self.cur_token_location_) + if marked: + lookups.append(lookup) + if not glyphs and not suffix: # eg., "sub f f i by" - return ([], prefix, []) + assert lookups == [] + return ([], prefix, [None] * len(prefix), []) else: - return (prefix, glyphs, suffix) + return (prefix, glyphs, lookups, 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, suffix = self.parse_glyph_pattern_() + prefix, glyphs, lookups, suffix = self.parse_glyph_pattern_() self.expect_symbol_(";") return ast.IgnoreSubstitutionRule(location, prefix, glyphs, suffix) raise ParserError("Expected \"substitute\"", self.next_token_location_) @@ -183,12 +199,19 @@ class Parser(object): def parse_substitute_(self): assert self.cur_token_ in {"substitute", "sub"} location = self.cur_token_location_ - old_prefix, old, old_suffix = self.parse_glyph_pattern_() - self.expect_keyword_("by") - new = self.parse_glyphclass_(accept_glyphname=True) + old_prefix, old, lookups, old_suffix = self.parse_glyph_pattern_() + if self.next_token_ == "by": + self.expect_keyword_("by") + new = [self.parse_glyphclass_(accept_glyphname=True)] + else: + new = [] self.expect_symbol_(";") - rule = ast.SubstitutionRule(location, old, [new]) + if len(new) is 0 and not any(lookups): + raise ParserError('Expected "by" or explicit lookup references', + self.cur_token_location_) + rule = ast.SubstitutionRule(location, old, new) rule.old_prefix, rule.old_suffix = old_prefix, old_suffix + rule.lookups = lookups return rule def parse_valuerecord_(self, vertical): diff --git a/Lib/fontTools/feaLib/parser_test.py b/Lib/fontTools/feaLib/parser_test.py index 1f39cb347..776b943c5 100644 --- a/Lib/fontTools/feaLib/parser_test.py +++ b/Lib/fontTools/feaLib/parser_test.py @@ -237,6 +237,7 @@ class ParserTest(unittest.TestCase): self.assertEqual(sub.old, [{"a"}]) self.assertEqual(sub.old_suffix, []) self.assertEqual(sub.new, [{"a.sc"}]) + self.assertEqual(sub.lookups, [None]) def test_substitute_single_format_b(self): # GSUB LookupType 1 doc = self.parse( @@ -248,6 +249,7 @@ class ParserTest(unittest.TestCase): self.assertEqual(sub.old, [{"one.fitted", "one.oldstyle"}]) self.assertEqual(sub.old_suffix, []) self.assertEqual(sub.new, [{"one"}]) + self.assertEqual(sub.lookups, [None]) def test_substitute_single_format_c(self): # GSUB LookupType 1 doc = self.parse( @@ -259,6 +261,7 @@ class ParserTest(unittest.TestCase): self.assertEqual(sub.old, [{"a", "b", "c", "d"}]) self.assertEqual(sub.old_suffix, []) self.assertEqual(sub.new, [{"A.sc", "B.sc", "C.sc", "D.sc"}]) + self.assertEqual(sub.lookups, [None]) def test_substitute_ligature(self): # GSUB LookupType 4 doc = self.parse("feature liga {substitute f f i by f_f_i;} liga;") @@ -267,10 +270,17 @@ class ParserTest(unittest.TestCase): self.assertEqual(sub.old, [{"f"}, {"f"}, {"i"}]) self.assertEqual(sub.old_suffix, []) self.assertEqual(sub.new, [{"f_f_i"}]) + self.assertEqual(sub.lookups, [None, None, None]) + + def test_substitute_lookups(self): + doc = Parser(self.getpath("spec5fi.fea")).parse() + [ligs, sub, feature] = doc.statements + self.assertEqual(feature.statements[0].lookups, [ligs, None, sub]) + self.assertEqual(feature.statements[1].lookups, [ligs, None, sub]) def test_substitute_missing_by(self): self.assertRaisesRegex( - ParserError, "Expected \"by\"", + ParserError, 'Expected "by" or explicit lookup references', self.parse, "feature liga {substitute f f i;} liga;") def test_valuerecord_format_a_horizontal(self): diff --git a/Lib/fontTools/feaLib/testdata/spec5fi.fea b/Lib/fontTools/feaLib/testdata/spec5fi.fea new file mode 100644 index 000000000..1d04fa855 --- /dev/null +++ b/Lib/fontTools/feaLib/testdata/spec5fi.fea @@ -0,0 +1,18 @@ +# OpenType Feature File specification, section 5.f.i, example 1 +# "Specifying a Chain Sub rule and marking sub-runs" +# http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html + +lookup CNTXT_LIGS { + substitute f i by f_i; + substitute c t by c_t; + } CNTXT_LIGS; + +lookup CNTXT_SUB { + substitute n by n.end; + substitute s by s.end; + } CNTXT_SUB; + +feature test { + substitute [a e i o u] f' lookup CNTXT_LIGS i' n' lookup CNTXT_SUB; + substitute [a e i o u] c' lookup CNTXT_LIGS t' s' lookup CNTXT_SUB; +} test;