[feaLib] Parse chained contextual substitution rules
This commit is contained in:
parent
c165a1e019
commit
00714511f0
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
18
Lib/fontTools/feaLib/testdata/spec5fi.fea
vendored
Normal file
18
Lib/fontTools/feaLib/testdata/spec5fi.fea
vendored
Normal 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;
|
Loading…
x
Reference in New Issue
Block a user