[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)
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):
def __init__(self, location, language, include_default, required):
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 "
"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 "
"ydieresis yacute breve "
"a_n_d ydieresis yacute breve "
"grave acute dieresis macron circumflex cedilla umlaut ogonek caron "
"damma hamza sukun kasratan lam_meem_jeem noon.final noon.initial "
).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
GSUB_2 GSUB_3 GSUB_6 GSUB_8
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
spec9b spec9c1 spec9c2 spec9c3
bug463 bug501 bug502 bug505 bug506 bug509

View File

@ -242,7 +242,7 @@ class Parser(object):
def parse_glyph_pattern_(self, vertical):
prefix, glyphs, lookups, values, suffix = ([], [], [], [], [])
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)
marked = False
if self.next_token_ == "'":
@ -291,13 +291,20 @@ class Parser(object):
if self.cur_token_ in ["substitute", "sub"]:
prefix, glyphs, lookups, values, suffix, hasMarks = \
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_(";")
if any(lookups):
if hasLookups:
raise FeatureLibError(
"No lookups can be specified for \"ignore sub\"",
location)
return ast.ChainContextSubstStatement(
location, prefix, glyphs, suffix, [])
return ast.IgnoreSubstStatement(location, chainContext)
raise FeatureLibError(
"Expected \"substitute\"", self.next_token_location_)

View File

@ -290,13 +290,16 @@ class ParserTest(unittest.TestCase):
self.assertIsNone(s.componentGlyphs)
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]
self.assertIsInstance(sub, ast.ChainContextSubstStatement)
self.assertEqual(glyphstr(sub.prefix), "e")
self.assertEqual(glyphstr(sub.glyphs), "t")
self.assertEqual(glyphstr(sub.suffix), "c")
self.assertEqual(sub.lookups, [])
self.assertIsInstance(sub, ast.IgnoreSubstStatement)
[(pref1, glyphs1, suff1), (pref2, glyphs2, suff2)] = sub.chainContexts
self.assertEqual(glyphstr(pref1), "e")
self.assertEqual(glyphstr(glyphs1), "t")
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):
doc = self.parse(
@ -304,11 +307,19 @@ class ParserTest(unittest.TestCase):
" ignore substitute f [a e] d' [a u]' [e y];"
"} test;")
sub = doc.statements[0].statements[0]
self.assertIsInstance(sub, ast.ChainContextSubstStatement)
self.assertEqual(glyphstr(sub.prefix), "f [a e]")
self.assertEqual(glyphstr(sub.glyphs), "d [a u]")
self.assertEqual(glyphstr(sub.suffix), "[e y]")
self.assertEqual(sub.lookups, [])
self.assertIsInstance(sub, ast.IgnoreSubstStatement)
[(prefix, glyphs, suffix)] = sub.chainContexts
self.assertEqual(glyphstr(prefix), "f [a e]")
self.assertEqual(glyphstr(glyphs), "d [a u]")
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):
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>