diff --git a/Lib/fontTools/feaLib/ast.py b/Lib/fontTools/feaLib/ast.py
index e52d768ec..bbe6e6e74 100644
--- a/Lib/fontTools/feaLib/ast.py
+++ b/Lib/fontTools/feaLib/ast.py
@@ -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,
)
diff --git a/Lib/fontTools/feaLib/parser.py b/Lib/fontTools/feaLib/parser.py
index b325b3649..c8f0acca3 100644
--- a/Lib/fontTools/feaLib/parser.py
+++ b/Lib/fontTools/feaLib/parser.py
@@ -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,
)
diff --git a/Tests/feaLib/data/GSUB_2.fea b/Tests/feaLib/data/GSUB_2.fea
index d2a3cb101..078cbec39 100644
--- a/Tests/feaLib/data/GSUB_2.fea
+++ b/Tests/feaLib/data/GSUB_2.fea
@@ -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;
diff --git a/Tests/feaLib/data/GSUB_2.ttx b/Tests/feaLib/data/GSUB_2.ttx
index b91c20fe0..b2bd21bd7 100644
--- a/Tests/feaLib/data/GSUB_2.ttx
+++ b/Tests/feaLib/data/GSUB_2.ttx
@@ -10,16 +10,19 @@
-
+
@@ -34,9 +37,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
@@ -57,6 +81,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tests/feaLib/parser_test.py b/Tests/feaLib/parser_test.py
index 6bf9c6a87..54c7c4426 100644
--- a/Tests/feaLib/parser_test.py
+++ b/Tests/feaLib/parser_test.py
@@ -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,