[feaLib] Parse chained contextual substitution rules

This commit is contained in:
Sascha Brawer 2015-08-11 12:22:07 +02:00
parent c165a1e019
commit 00714511f0
4 changed files with 61 additions and 9 deletions

View File

@ -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):

View File

@ -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):

View File

@ -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):

View File

@ -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;