Merge pull request #3103 from fonttools/multiple-subst-classes
feaLib: support multiple substitution with classes
This commit is contained in:
commit
b6209e0510
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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,
|
||||||
)
|
)
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user