Merge pull request #3103 from fonttools/multiple-subst-classes

feaLib: support multiple substitution with classes
This commit is contained in:
خالد حسني (Khaled Hosny) 2023-05-09 16:15:33 +03:00 committed by GitHub
commit b6209e0510
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 161 additions and 32 deletions

View File

@ -1256,23 +1256,29 @@ class MultipleSubstStatement(Statement):
"""Calls the builder object's ``add_multiple_subst`` callback."""
prefix = [p.glyphSet() for p in self.prefix]
suffix = [s.glyphSet() for s in self.suffix]
if not self.replacement and hasattr(self.glyph, "glyphSet"):
for glyph in self.glyph.glyphSet():
builder.add_multiple_subst(
self.location,
prefix,
glyph,
suffix,
self.replacement,
self.forceChain,
)
if hasattr(self.glyph, "glyphSet"):
originals = self.glyph.glyphSet()
else:
originals = [self.glyph]
count = len(originals)
replaces = []
for r in self.replacement:
if hasattr(r, "glyphSet"):
replace = r.glyphSet()
else:
replace = [r]
if len(replace) == 1 and len(replace) != count:
replace = replace * count
replaces.append(replace)
replaces = list(zip(*replaces))
for i, original in enumerate(originals):
builder.add_multiple_subst(
self.location,
prefix,
self.glyph,
original,
suffix,
self.replacement,
replaces and replaces[i] or (),
self.forceChain,
)

View File

@ -925,22 +925,27 @@ class Parser(object):
# GSUB lookup type 2: Multiple substitution.
# Format: "substitute f_f_i by f f i;"
if (
not reverse
and len(old) == 1
and len(old[0].glyphSet()) == 1
and len(new) > 1
and max([len(n.glyphSet()) for n in new]) == 1
and num_lookups == 0
):
#
# GlyphsApp introduces two additional formats:
# Format 1: "substitute [f_i f_l] by [f f] [i l];"
# Format 2: "substitute [f_i f_l] by f [i l];"
# http://handbook.glyphsapp.com/en/layout/multiple-substitution-with-classes/
if not reverse and len(old) == 1 and len(new) > 1 and num_lookups == 0:
count = len(old[0].glyphSet())
for n in new:
if not list(n.glyphSet()):
raise FeatureLibError("Empty class in replacement", location)
if not isinstance(n, self.ast.GlyphName) and len(n.glyphSet()) != count:
raise FeatureLibError(
f'Expected a glyph class with {count} elements after "by", '
f"but found a glyph class with {len(n.glyphSet())} elements",
location,
)
return self.ast.MultipleSubstStatement(
old_prefix,
tuple(old[0].glyphSet())[0],
old[0],
old_suffix,
tuple([list(n.glyphSet())[0] for n in new]),
new,
forceChain=hasMarks,
location=location,
)

View File

@ -12,3 +12,20 @@ feature f2 {
sub f_i by f i;
sub f_f_i by f f i;
} f2;
feature f3 {
sub [f_i f_l f_f_i f_f_l] by f [i l f_i f_l];
} f3;
feature f4 {
sub [f_i f_l f_f_i f_f_l] by [f f f_f f_f] [i l i l];
} f4;
@class = [f_i f_l];
lookup l1 {
sub @class by f [i l];
} l1;
feature f5 {
sub @class' lookup l1 [i l];
} f5;

View File

@ -10,16 +10,19 @@
<Script>
<DefaultLangSys>
<ReqFeatureIndex value="65535"/>
<!-- FeatureCount=2 -->
<!-- FeatureCount=5 -->
<FeatureIndex index="0" value="0"/>
<FeatureIndex index="1" value="1"/>
<FeatureIndex index="2" value="2"/>
<FeatureIndex index="3" value="3"/>
<FeatureIndex index="4" value="4"/>
</DefaultLangSys>
<!-- LangSysCount=0 -->
</Script>
</ScriptRecord>
</ScriptList>
<FeatureList>
<!-- FeatureCount=2 -->
<!-- FeatureCount=5 -->
<FeatureRecord index="0">
<FeatureTag value="f1 "/>
<Feature>
@ -34,9 +37,30 @@
<LookupListIndex index="0" value="1"/>
</Feature>
</FeatureRecord>
<FeatureRecord index="2">
<FeatureTag value="f3 "/>
<Feature>
<!-- LookupCount=1 -->
<LookupListIndex index="0" value="2"/>
</Feature>
</FeatureRecord>
<FeatureRecord index="3">
<FeatureTag value="f4 "/>
<Feature>
<!-- LookupCount=1 -->
<LookupListIndex index="0" value="3"/>
</Feature>
</FeatureRecord>
<FeatureRecord index="4">
<FeatureTag value="f5 "/>
<Feature>
<!-- LookupCount=1 -->
<LookupListIndex index="0" value="5"/>
</Feature>
</FeatureRecord>
</FeatureList>
<LookupList>
<!-- LookupCount=2 -->
<!-- LookupCount=6 -->
<Lookup index="0">
<LookupType value="2"/>
<LookupFlag value="0"/>
@ -57,6 +81,60 @@
<Substitution in="f_i" out="f,i"/>
</MultipleSubst>
</Lookup>
<Lookup index="2">
<LookupType value="2"/>
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<MultipleSubst index="0">
<Substitution in="f_f_i" out="f,f_i"/>
<Substitution in="f_f_l" out="f,f_l"/>
<Substitution in="f_i" out="f,i"/>
<Substitution in="f_l" out="f,l"/>
</MultipleSubst>
</Lookup>
<Lookup index="3">
<LookupType value="2"/>
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<MultipleSubst index="0">
<Substitution in="f_f_i" out="f_f,i"/>
<Substitution in="f_f_l" out="f_f,l"/>
<Substitution in="f_i" out="f,i"/>
<Substitution in="f_l" out="f,l"/>
</MultipleSubst>
</Lookup>
<Lookup index="4">
<LookupType value="2"/>
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<MultipleSubst index="0">
<Substitution in="f_i" out="f,i"/>
<Substitution in="f_l" out="f,l"/>
</MultipleSubst>
</Lookup>
<Lookup index="5">
<LookupType value="6"/>
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<ChainContextSubst index="0" Format="3">
<!-- BacktrackGlyphCount=0 -->
<!-- InputGlyphCount=1 -->
<InputCoverage index="0">
<Glyph value="f_l"/>
<Glyph value="f_i"/>
</InputCoverage>
<!-- LookAheadGlyphCount=1 -->
<LookAheadCoverage index="0">
<Glyph value="i"/>
<Glyph value="l"/>
</LookAheadCoverage>
<!-- SubstCount=1 -->
<SubstLookupRecord index="0">
<SequenceIndex value="0"/>
<LookupListIndex value="4"/>
</SubstLookupRecord>
</ChainContextSubst>
</Lookup>
</LookupList>
</GSUB>

View File

@ -44,7 +44,7 @@ GLYPHNAMES = (
a.swash b.swash x.swash y.swash z.swash
foobar foo.09 foo.1234 foo.9876
one two five six acute grave dieresis umlaut cedilla ogonek macron
a_f_f_i o_f_f_i f_i f_f_i one.fitted one.oldstyle a.1 a.2 a.3 c_t
a_f_f_i o_f_f_i f_i f_l f_f_i one.fitted one.oldstyle a.1 a.2 a.3 c_t
PRE SUF FIX BACK TRACK LOOK AHEAD ampersand ampersand.1 ampersand.2
cid00001 cid00002 cid00003 cid00004 cid00005 cid00006 cid00007
cid12345 cid78987 cid00999 cid01000 cid01001 cid00998 cid00995
@ -1610,24 +1610,47 @@ class ParserTest(unittest.TestCase):
doc = self.parse("lookup Look {substitute f_f_i by f f i;} Look;")
sub = doc.statements[0].statements[0]
self.assertIsInstance(sub, ast.MultipleSubstStatement)
self.assertEqual(sub.glyph, "f_f_i")
self.assertEqual(sub.replacement, ("f", "f", "i"))
self.assertEqual(glyphstr([sub.glyph]), "f_f_i")
self.assertEqual(glyphstr(sub.replacement), "f f i")
def test_substitute_multiple_chained(self): # chain to GSUB LookupType 2
doc = self.parse("lookup L {sub [A-C] f_f_i' [X-Z] by f f i;} L;")
sub = doc.statements[0].statements[0]
self.assertIsInstance(sub, ast.MultipleSubstStatement)
self.assertEqual(sub.glyph, "f_f_i")
self.assertEqual(sub.replacement, ("f", "f", "i"))
self.assertEqual(glyphstr([sub.glyph]), "f_f_i")
self.assertEqual(glyphstr(sub.replacement), "f f i")
def test_substitute_multiple_force_chained(self):
doc = self.parse("lookup L {sub f_f_i' by f f i;} L;")
sub = doc.statements[0].statements[0]
self.assertIsInstance(sub, ast.MultipleSubstStatement)
self.assertEqual(sub.glyph, "f_f_i")
self.assertEqual(sub.replacement, ("f", "f", "i"))
self.assertEqual(glyphstr([sub.glyph]), "f_f_i")
self.assertEqual(glyphstr(sub.replacement), "f f i")
self.assertEqual(sub.asFea(), "sub f_f_i' by f f i;")
def test_substitute_multiple_classes(self):
doc = self.parse("lookup Look {substitute [f_i f_l] by [f f] [i l];} Look;")
sub = doc.statements[0].statements[0]
self.assertIsInstance(sub, ast.MultipleSubstStatement)
self.assertEqual(glyphstr([sub.glyph]), "[f_i f_l]")
self.assertEqual(glyphstr(sub.replacement), "[f f] [i l]")
def test_substitute_multiple_classes_mixed(self):
doc = self.parse("lookup Look {substitute [f_i f_l] by f [i l];} Look;")
sub = doc.statements[0].statements[0]
self.assertIsInstance(sub, ast.MultipleSubstStatement)
self.assertEqual(glyphstr([sub.glyph]), "[f_i f_l]")
self.assertEqual(glyphstr(sub.replacement), "f [i l]")
def test_substitute_multiple_classes_mismatch(self):
self.assertRaisesRegex(
FeatureLibError,
'Expected a glyph class with 2 elements after "by", '
"but found a glyph class with 1 elements",
self.parse,
"lookup Look {substitute [f_i f_l] by [f] [i l];} Look;",
)
def test_substitute_multiple_by_mutliple(self):
self.assertRaisesRegex(
FeatureLibError,