[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:
parent
a862f70880
commit
1ddfe24338
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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_)
|
||||
|
||||
|
@ -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;")
|
||||
|
9
Lib/fontTools/feaLib/testdata/spec5f_ii_3.fea
vendored
Normal file
9
Lib/fontTools/feaLib/testdata/spec5f_ii_3.fea
vendored
Normal 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;
|
155
Lib/fontTools/feaLib/testdata/spec5f_ii_3.ttx
vendored
Normal file
155
Lib/fontTools/feaLib/testdata/spec5f_ii_3.ttx
vendored
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user