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.""" """Calls the builder object's ``add_multiple_subst`` callback."""
prefix = [p.glyphSet() for p in self.prefix] prefix = [p.glyphSet() for p in self.prefix]
suffix = [s.glyphSet() for s in self.suffix] suffix = [s.glyphSet() for s in self.suffix]
if not self.replacement and hasattr(self.glyph, "glyphSet"): if hasattr(self.glyph, "glyphSet"):
for glyph in self.glyph.glyphSet(): originals = self.glyph.glyphSet()
builder.add_multiple_subst(
self.location,
prefix,
glyph,
suffix,
self.replacement,
self.forceChain,
)
else: 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( builder.add_multiple_subst(
self.location, self.location,
prefix, prefix,
self.glyph, original,
suffix, suffix,
self.replacement, replaces and replaces[i] or (),
self.forceChain, self.forceChain,
) )

View File

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

View File

@ -12,3 +12,20 @@ feature f2 {
sub f_i by f i; sub f_i by f i;
sub f_f_i by f f i; sub f_f_i by f f i;
} f2; } 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> <Script>
<DefaultLangSys> <DefaultLangSys>
<ReqFeatureIndex value="65535"/> <ReqFeatureIndex value="65535"/>
<!-- FeatureCount=2 --> <!-- FeatureCount=5 -->
<FeatureIndex index="0" value="0"/> <FeatureIndex index="0" value="0"/>
<FeatureIndex index="1" value="1"/> <FeatureIndex index="1" value="1"/>
<FeatureIndex index="2" value="2"/>
<FeatureIndex index="3" value="3"/>
<FeatureIndex index="4" value="4"/>
</DefaultLangSys> </DefaultLangSys>
<!-- LangSysCount=0 --> <!-- LangSysCount=0 -->
</Script> </Script>
</ScriptRecord> </ScriptRecord>
</ScriptList> </ScriptList>
<FeatureList> <FeatureList>
<!-- FeatureCount=2 --> <!-- FeatureCount=5 -->
<FeatureRecord index="0"> <FeatureRecord index="0">
<FeatureTag value="f1 "/> <FeatureTag value="f1 "/>
<Feature> <Feature>
@ -34,9 +37,30 @@
<LookupListIndex index="0" value="1"/> <LookupListIndex index="0" value="1"/>
</Feature> </Feature>
</FeatureRecord> </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> </FeatureList>
<LookupList> <LookupList>
<!-- LookupCount=2 --> <!-- LookupCount=6 -->
<Lookup index="0"> <Lookup index="0">
<LookupType value="2"/> <LookupType value="2"/>
<LookupFlag value="0"/> <LookupFlag value="0"/>
@ -57,6 +81,60 @@
<Substitution in="f_i" out="f,i"/> <Substitution in="f_i" out="f,i"/>
</MultipleSubst> </MultipleSubst>
</Lookup> </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> </LookupList>
</GSUB> </GSUB>

View File

@ -44,7 +44,7 @@ GLYPHNAMES = (
a.swash b.swash x.swash y.swash z.swash a.swash b.swash x.swash y.swash z.swash
foobar foo.09 foo.1234 foo.9876 foobar foo.09 foo.1234 foo.9876
one two five six acute grave dieresis umlaut cedilla ogonek macron 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 PRE SUF FIX BACK TRACK LOOK AHEAD ampersand ampersand.1 ampersand.2
cid00001 cid00002 cid00003 cid00004 cid00005 cid00006 cid00007 cid00001 cid00002 cid00003 cid00004 cid00005 cid00006 cid00007
cid12345 cid78987 cid00999 cid01000 cid01001 cid00998 cid00995 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;") doc = self.parse("lookup Look {substitute f_f_i by f f i;} Look;")
sub = doc.statements[0].statements[0] sub = doc.statements[0].statements[0]
self.assertIsInstance(sub, ast.MultipleSubstStatement) self.assertIsInstance(sub, ast.MultipleSubstStatement)
self.assertEqual(sub.glyph, "f_f_i") self.assertEqual(glyphstr([sub.glyph]), "f_f_i")
self.assertEqual(sub.replacement, ("f", "f", "i")) self.assertEqual(glyphstr(sub.replacement), "f f i")
def test_substitute_multiple_chained(self): # chain to GSUB LookupType 2 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;") doc = self.parse("lookup L {sub [A-C] f_f_i' [X-Z] by f f i;} L;")
sub = doc.statements[0].statements[0] sub = doc.statements[0].statements[0]
self.assertIsInstance(sub, ast.MultipleSubstStatement) self.assertIsInstance(sub, ast.MultipleSubstStatement)
self.assertEqual(sub.glyph, "f_f_i") self.assertEqual(glyphstr([sub.glyph]), "f_f_i")
self.assertEqual(sub.replacement, ("f", "f", "i")) self.assertEqual(glyphstr(sub.replacement), "f f i")
def test_substitute_multiple_force_chained(self): def test_substitute_multiple_force_chained(self):
doc = self.parse("lookup L {sub f_f_i' by f f i;} L;") doc = self.parse("lookup L {sub f_f_i' by f f i;} L;")
sub = doc.statements[0].statements[0] sub = doc.statements[0].statements[0]
self.assertIsInstance(sub, ast.MultipleSubstStatement) self.assertIsInstance(sub, ast.MultipleSubstStatement)
self.assertEqual(sub.glyph, "f_f_i") self.assertEqual(glyphstr([sub.glyph]), "f_f_i")
self.assertEqual(sub.replacement, ("f", "f", "i")) self.assertEqual(glyphstr(sub.replacement), "f f i")
self.assertEqual(sub.asFea(), "sub f_f_i' by 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): def test_substitute_multiple_by_mutliple(self):
self.assertRaisesRegex( self.assertRaisesRegex(
FeatureLibError, FeatureLibError,