[feaLib] Support multiple chain contexts after ignore sub

Although this construct is in violation of the `ignore sub` grammar
given by the current OpenType Feature File syntax specification,
the very same specification document illustrates (in example 3
of section 5.f.ii) the `ignore sub` statement with a comma-separated
list of backgrack/input/lookahead triples.

See https://github.com/adobe-type-tools/afdko/issues/105 for a request
to amend the OpenType Feature File syntax specification.

After this code change, feaLib can now parse testdata/spec5f_ii_3.fea;
the output is identical to what is generated by Adobe's makeotf tool.

https://github.com/behdad/fonttools/issues/503
This commit is contained in:
Sascha Brawer 2016-02-05 11:02:29 +01:00
parent a862f70880
commit 1ddfe24338
6 changed files with 213 additions and 17 deletions

View File

@ -273,6 +273,20 @@ class FeatureReferenceStatement(Statement):
builder.add_feature_reference(self.location, self.featureName) builder.add_feature_reference(self.location, self.featureName)
class IgnoreSubstStatement(Statement):
def __init__(self, location, chainContexts):
Statement.__init__(self, location)
self.chainContexts = chainContexts
def build(self, builder):
for prefix, glyphs, suffix in self.chainContexts:
prefix = [p.glyphSet() for p in prefix]
glyphs = [g.glyphSet() for g in glyphs]
suffix = [s.glyphSet() for s in suffix]
builder.add_chain_context_subst(
self.location, prefix, glyphs, suffix, [])
class LanguageStatement(Statement): class LanguageStatement(Statement):
def __init__(self, location, language, include_default, required): def __init__(self, location, language, include_default, required):
Statement.__init__(self, location) Statement.__init__(self, location)

View File

@ -31,7 +31,7 @@ def makeTTFont():
"a.alt1 a.alt2 a.alt3 b.alt c.mid d.alt d.mid e.begin e.mid " "a.alt1 a.alt2 a.alt3 b.alt c.mid d.alt d.mid e.begin e.mid "
"n.end s.end Eng Eng.alt1 Eng.alt2 Eng.alt3 " "n.end s.end Eng Eng.alt1 Eng.alt2 Eng.alt3 "
"f_l c_h c_k c_s c_t f_f f_f_i f_f_l f_i o_f_f_i s_t f_i.begin " "f_l c_h c_k c_s c_t f_f f_f_i f_f_l f_i o_f_f_i s_t f_i.begin "
"ydieresis yacute breve " "a_n_d ydieresis yacute breve "
"grave acute dieresis macron circumflex cedilla umlaut ogonek caron " "grave acute dieresis macron circumflex cedilla umlaut ogonek caron "
"damma hamza sukun kasratan lam_meem_jeem noon.final noon.initial " "damma hamza sukun kasratan lam_meem_jeem noon.final noon.initial "
).split() ).split()
@ -49,7 +49,7 @@ class BuilderTest(unittest.TestCase):
GPOS_1 GPOS_1_zero GPOS_2 GPOS_2b GPOS_3 GPOS_4 GPOS_5 GPOS_6 GPOS_8 GPOS_1 GPOS_1_zero GPOS_2 GPOS_2b GPOS_3 GPOS_4 GPOS_5 GPOS_6 GPOS_8
GSUB_2 GSUB_3 GSUB_6 GSUB_8 GSUB_2 GSUB_3 GSUB_6 GSUB_8
spec4h1 spec5d1 spec5d2 spec5fi1 spec5fi2 spec5fi3 spec5fi4 spec4h1 spec5d1 spec5d2 spec5fi1 spec5fi2 spec5fi3 spec5fi4
spec5f_ii_1 spec5f_ii_2 spec5f_ii_1 spec5f_ii_2 spec5f_ii_3
spec5h1 spec6b_ii spec6d2 spec6e spec6f spec6h_ii spec6h_iii_1 spec8a spec5h1 spec6b_ii spec6d2 spec6e spec6f spec6h_ii spec6h_iii_1 spec8a
spec9b spec9c1 spec9c2 spec9c3 spec9b spec9c1 spec9c2 spec9c3
bug463 bug501 bug502 bug505 bug506 bug509 bug463 bug501 bug502 bug505 bug506 bug509

View File

@ -242,7 +242,7 @@ class Parser(object):
def parse_glyph_pattern_(self, vertical): def parse_glyph_pattern_(self, vertical):
prefix, glyphs, lookups, values, suffix = ([], [], [], [], []) prefix, glyphs, lookups, values, suffix = ([], [], [], [], [])
hasMarks = False hasMarks = False
while self.next_token_ not in {"by", "from", ";"}: while self.next_token_ not in {"by", "from", ";", ","}:
gc = self.parse_glyphclass_(accept_glyphname=True) gc = self.parse_glyphclass_(accept_glyphname=True)
marked = False marked = False
if self.next_token_ == "'": if self.next_token_ == "'":
@ -291,13 +291,20 @@ class Parser(object):
if self.cur_token_ in ["substitute", "sub"]: if self.cur_token_ in ["substitute", "sub"]:
prefix, glyphs, lookups, values, suffix, hasMarks = \ prefix, glyphs, lookups, values, suffix, hasMarks = \
self.parse_glyph_pattern_(vertical=False) self.parse_glyph_pattern_(vertical=False)
chainContext = [(prefix, glyphs, suffix)]
hasLookups = any(lookups)
while self.next_token_ == ",":
self.expect_symbol_(",")
prefix, glyphs, lookups, values, suffix, hasMarks = \
self.parse_glyph_pattern_(vertical=False)
chainContext.append((prefix, glyphs, suffix))
hasLookups = hasLookups or any(lookups)
self.expect_symbol_(";") self.expect_symbol_(";")
if any(lookups): if hasLookups:
raise FeatureLibError( raise FeatureLibError(
"No lookups can be specified for \"ignore sub\"", "No lookups can be specified for \"ignore sub\"",
location) location)
return ast.ChainContextSubstStatement( return ast.IgnoreSubstStatement(location, chainContext)
location, prefix, glyphs, suffix, [])
raise FeatureLibError( raise FeatureLibError(
"Expected \"substitute\"", self.next_token_location_) "Expected \"substitute\"", self.next_token_location_)

View File

@ -290,13 +290,16 @@ class ParserTest(unittest.TestCase):
self.assertIsNone(s.componentGlyphs) self.assertIsNone(s.componentGlyphs)
def test_ignore_sub(self): def test_ignore_sub(self):
doc = self.parse("feature test {ignore sub e t' c;} test;") doc = self.parse("feature test {ignore sub e t' c, q u' u' x;} test;")
sub = doc.statements[0].statements[0] sub = doc.statements[0].statements[0]
self.assertIsInstance(sub, ast.ChainContextSubstStatement) self.assertIsInstance(sub, ast.IgnoreSubstStatement)
self.assertEqual(glyphstr(sub.prefix), "e") [(pref1, glyphs1, suff1), (pref2, glyphs2, suff2)] = sub.chainContexts
self.assertEqual(glyphstr(sub.glyphs), "t") self.assertEqual(glyphstr(pref1), "e")
self.assertEqual(glyphstr(sub.suffix), "c") self.assertEqual(glyphstr(glyphs1), "t")
self.assertEqual(sub.lookups, []) self.assertEqual(glyphstr(suff1), "c")
self.assertEqual(glyphstr(pref2), "q")
self.assertEqual(glyphstr(glyphs2), "u u")
self.assertEqual(glyphstr(suff2), "x")
def test_ignore_substitute(self): def test_ignore_substitute(self):
doc = self.parse( doc = self.parse(
@ -304,11 +307,19 @@ class ParserTest(unittest.TestCase):
" ignore substitute f [a e] d' [a u]' [e y];" " ignore substitute f [a e] d' [a u]' [e y];"
"} test;") "} test;")
sub = doc.statements[0].statements[0] sub = doc.statements[0].statements[0]
self.assertIsInstance(sub, ast.ChainContextSubstStatement) self.assertIsInstance(sub, ast.IgnoreSubstStatement)
self.assertEqual(glyphstr(sub.prefix), "f [a e]") [(prefix, glyphs, suffix)] = sub.chainContexts
self.assertEqual(glyphstr(sub.glyphs), "d [a u]") self.assertEqual(glyphstr(prefix), "f [a e]")
self.assertEqual(glyphstr(sub.suffix), "[e y]") self.assertEqual(glyphstr(glyphs), "d [a u]")
self.assertEqual(sub.lookups, []) self.assertEqual(glyphstr(suffix), "[e y]")
def test_ignore_substitute_with_lookup(self):
self.assertRaisesRegex(
FeatureLibError,
'No lookups can be specified for "ignore sub"',
self.parse,
"lookup L { sub [A A.sc] by a; } L;"
"feature test { ignore sub f' i', A' lookup L; } test;")
def test_language(self): def test_language(self):
doc = self.parse("feature test {language DEU;} test;") doc = self.parse("feature test {language DEU;} test;")

View File

@ -0,0 +1,9 @@
# OpenType Feature File specification, section 5.f.ii, example 3
# "Specifying exceptions to the Chain Sub rule"
# http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html
feature test {
@LETTER = [a-z];
ignore substitute @LETTER a' n' d', a' n' d' @LETTER;
substitute a' n' d' by a_n_d;
} test;

View File

@ -0,0 +1,155 @@
<?xml version="1.0" encoding="UTF-8"?>
<ttFont sfntVersion="true" ttLibVersion="3.0">
<GSUB>
<Version value="1.0"/>
<ScriptList>
<!-- ScriptCount=1 -->
<ScriptRecord index="0">
<ScriptTag value="DFLT"/>
<Script>
<DefaultLangSys>
<ReqFeatureIndex value="65535"/>
<!-- FeatureCount=1 -->
<FeatureIndex index="0" value="0"/>
</DefaultLangSys>
<!-- LangSysCount=0 -->
</Script>
</ScriptRecord>
</ScriptList>
<FeatureList>
<!-- FeatureCount=1 -->
<FeatureRecord index="0">
<FeatureTag value="test"/>
<Feature>
<!-- LookupCount=1 -->
<LookupListIndex index="0" value="0"/>
</Feature>
</FeatureRecord>
</FeatureList>
<LookupList>
<!-- LookupCount=2 -->
<Lookup index="0">
<!-- LookupType=6 -->
<LookupFlag value="0"/>
<!-- SubTableCount=3 -->
<ChainContextSubst index="0" Format="3">
<!-- BacktrackGlyphCount=1 -->
<BacktrackCoverage index="0">
<Glyph value="a"/>
<Glyph value="b"/>
<Glyph value="c"/>
<Glyph value="d"/>
<Glyph value="e"/>
<Glyph value="f"/>
<Glyph value="g"/>
<Glyph value="h"/>
<Glyph value="i"/>
<Glyph value="j"/>
<Glyph value="k"/>
<Glyph value="l"/>
<Glyph value="m"/>
<Glyph value="n"/>
<Glyph value="o"/>
<Glyph value="p"/>
<Glyph value="q"/>
<Glyph value="r"/>
<Glyph value="s"/>
<Glyph value="t"/>
<Glyph value="u"/>
<Glyph value="v"/>
<Glyph value="w"/>
<Glyph value="x"/>
<Glyph value="y"/>
<Glyph value="z"/>
</BacktrackCoverage>
<!-- InputGlyphCount=3 -->
<InputCoverage index="0">
<Glyph value="a"/>
</InputCoverage>
<InputCoverage index="1">
<Glyph value="n"/>
</InputCoverage>
<InputCoverage index="2">
<Glyph value="d"/>
</InputCoverage>
<!-- LookAheadGlyphCount=0 -->
<!-- SubstCount=0 -->
</ChainContextSubst>
<ChainContextSubst index="1" Format="3">
<!-- BacktrackGlyphCount=0 -->
<!-- InputGlyphCount=3 -->
<InputCoverage index="0">
<Glyph value="a"/>
</InputCoverage>
<InputCoverage index="1">
<Glyph value="n"/>
</InputCoverage>
<InputCoverage index="2">
<Glyph value="d"/>
</InputCoverage>
<!-- LookAheadGlyphCount=1 -->
<LookAheadCoverage index="0">
<Glyph value="a"/>
<Glyph value="b"/>
<Glyph value="c"/>
<Glyph value="d"/>
<Glyph value="e"/>
<Glyph value="f"/>
<Glyph value="g"/>
<Glyph value="h"/>
<Glyph value="i"/>
<Glyph value="j"/>
<Glyph value="k"/>
<Glyph value="l"/>
<Glyph value="m"/>
<Glyph value="n"/>
<Glyph value="o"/>
<Glyph value="p"/>
<Glyph value="q"/>
<Glyph value="r"/>
<Glyph value="s"/>
<Glyph value="t"/>
<Glyph value="u"/>
<Glyph value="v"/>
<Glyph value="w"/>
<Glyph value="x"/>
<Glyph value="y"/>
<Glyph value="z"/>
</LookAheadCoverage>
<!-- SubstCount=0 -->
</ChainContextSubst>
<ChainContextSubst index="2" Format="3">
<!-- BacktrackGlyphCount=0 -->
<!-- InputGlyphCount=3 -->
<InputCoverage index="0">
<Glyph value="a"/>
</InputCoverage>
<InputCoverage index="1">
<Glyph value="n"/>
</InputCoverage>
<InputCoverage index="2">
<Glyph value="d"/>
</InputCoverage>
<!-- LookAheadGlyphCount=0 -->
<!-- SubstCount=1 -->
<SubstLookupRecord index="0">
<SequenceIndex value="0"/>
<LookupListIndex value="1"/>
</SubstLookupRecord>
</ChainContextSubst>
</Lookup>
<Lookup index="1">
<!-- LookupType=4 -->
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<LigatureSubst index="0">
<LigatureSet glyph="a">
<Ligature components="n,d" glyph="a_n_d"/>
</LigatureSet>
</LigatureSubst>
</Lookup>
</LookupList>
</GSUB>
</ttFont>