diff --git a/Lib/fontTools/feaLib/ast.py b/Lib/fontTools/feaLib/ast.py
index a69580013..62c9992c8 100644
--- a/Lib/fontTools/feaLib/ast.py
+++ b/Lib/fontTools/feaLib/ast.py
@@ -198,8 +198,8 @@ class ChainContextPosStatement(Statement):
class ChainContextSubstStatement(Statement):
def __init__(self, location, prefix, glyphs, suffix, lookups):
Statement.__init__(self, location)
- self.glyphs, self.lookups = glyphs, lookups
- self.prefix, self.suffix = prefix, suffix
+ self.prefix, self.glyphs, self.suffix = prefix, glyphs, suffix
+ self.lookups = lookups
def build(self, builder):
prefix = [p.glyphSet() for p in self.prefix]
@@ -373,12 +373,14 @@ class ReverseChainSingleSubstStatement(Statement):
class SingleSubstStatement(Statement):
- def __init__(self, location, mapping):
+ def __init__(self, location, mapping, prefix, suffix):
Statement.__init__(self, location)
- self.mapping = mapping
+ self.mapping, self.prefix, self.suffix = mapping, prefix, suffix
def build(self, builder):
- builder.add_single_subst(self.location, self.mapping)
+ prefix = [p.glyphSet() for p in self.prefix]
+ suffix = [s.glyphSet() for s in self.suffix]
+ builder.add_single_subst(self.location, prefix, suffix, self.mapping)
class ScriptStatement(Statement):
diff --git a/Lib/fontTools/feaLib/builder.py b/Lib/fontTools/feaLib/builder.py
index 2569c18e4..5c524fe33 100644
--- a/Lib/fontTools/feaLib/builder.py
+++ b/Lib/fontTools/feaLib/builder.py
@@ -51,6 +51,13 @@ class Builder(object):
elif "GDEF" in self.font:
del self.font["GDEF"]
+ def get_chained_lookup_(self, location, builder_class):
+ result = builder_class(self.font, location)
+ result.lookupflag = self.lookupflag_
+ result.markFilterSet = self.lookupflag_markFilterSet_
+ self.lookups_.append(result)
+ return result
+
def get_lookup_(self, location, builder_class):
if (self.cur_lookup_ and
type(self.cur_lookup_) == builder_class and
@@ -410,7 +417,14 @@ class Builder(object):
lookup = self.get_lookup_(location, ReverseChainSingleSubstBuilder)
lookup.substitutions.append((old_prefix, old_suffix, mapping))
- def add_single_subst(self, location, mapping):
+ def add_single_subst(self, location, prefix, suffix, mapping):
+ if prefix or suffix:
+ sub = self.get_chained_lookup_(location, SingleSubstBuilder)
+ sub.mapping.update(mapping)
+ lookup = self.get_lookup_(location, ChainContextSubstBuilder)
+ lookup.substitutions.append(
+ (prefix, [mapping.keys()], suffix, [sub]))
+ return
lookup = self.get_lookup_(location, SingleSubstBuilder)
for (from_glyph, to_glyph) in mapping.items():
if from_glyph in lookup.mapping:
diff --git a/Lib/fontTools/feaLib/builder_test.py b/Lib/fontTools/feaLib/builder_test.py
index 25e931a90..9ef45b40c 100644
--- a/Lib/fontTools/feaLib/builder_test.py
+++ b/Lib/fontTools/feaLib/builder_test.py
@@ -174,7 +174,7 @@ class BuilderTest(unittest.TestCase):
self.expect_ttx(font, self.getpath("GPOS_%s.ttx" % name))
def test_spec(self):
- for name in "4h1 5d1 5d2 5fi1 5h1 6d2 6e 6f 6h_ii".split():
+ for name in "4h1 5d1 5d2 5fi1 5fi2 5h1 6d2 6e 6f 6h_ii".split():
font = makeTTFont()
addOpenTypeFeatures(self.getpath("spec%s.fea" % name), font)
self.expect_ttx(font, self.getpath("spec%s.ttx" % name))
diff --git a/Lib/fontTools/feaLib/parser.py b/Lib/fontTools/feaLib/parser.py
index 94a7c4ed0..178c50e69 100644
--- a/Lib/fontTools/feaLib/parser.py
+++ b/Lib/fontTools/feaLib/parser.py
@@ -446,10 +446,10 @@ class Parser(object):
keyword = self.expect_keyword_("by")
while self.next_token_ != ";":
gc = self.parse_glyphclass_(accept_glyphname=True)
- new.append(gc.glyphSet())
+ new.append(gc)
elif self.next_token_ == "from":
keyword = self.expect_keyword_("from")
- new = [self.parse_glyphclass_(accept_glyphname=False).glyphSet()]
+ new = [self.parse_glyphclass_(accept_glyphname=False)]
else:
keyword = None
self.expect_symbol_(";")
@@ -475,7 +475,7 @@ class Parser(object):
location)
return ast.AlternateSubstStatement(location,
list(old[0].glyphSet())[0],
- new[0])
+ new[0].glyphSet())
num_lookups = len([l for l in lookups if l is not None])
@@ -483,10 +483,10 @@ class Parser(object):
# Format A: "substitute a by a.sc;"
# Format B: "substitute [one.fitted one.oldstyle] by one;"
# Format C: "substitute [a-d] by [A.sc-D.sc];"
- if (not reverse and len(old_prefix) == 0 and len(old_suffix) == 0 and
- len(old) == 1 and len(new) == 1 and num_lookups == 0):
+ if (not reverse and len(old) == 1 and len(new) == 1 and
+ num_lookups == 0):
glyphs = sorted(list(old[0].glyphSet()))
- replacements = sorted(list(new[0]))
+ replacements = sorted(list(new[0].glyphSet()))
if len(replacements) == 1:
replacements = replacements * len(glyphs)
if len(glyphs) != len(replacements):
@@ -495,24 +495,28 @@ class Parser(object):
'but found a glyph class with %d elements' %
(len(glyphs), len(replacements)), location)
return ast.SingleSubstStatement(location,
- dict(zip(glyphs, replacements)))
+ dict(zip(glyphs, replacements)),
+ old_prefix, old_suffix)
# GSUB lookup type 2: Multiple substitution.
# Format: "substitute f_f_i by f f i;"
if (not reverse and len(old_prefix) == 0 and len(old_suffix) == 0 and
len(old) == 1 and len(old[0].glyphSet()) == 1 and
- len(new) > 1 and max([len(n) for n in new]) == 1 and
+ len(new) > 1 and max([len(n.glyphSet()) for n in new]) == 1 and
num_lookups == 0):
- return ast.MultipleSubstStatement(location,
- tuple(old[0].glyphSet())[0],
- tuple([list(n)[0] for n in new]))
+ return ast.MultipleSubstStatement(
+ location,
+ tuple(old[0].glyphSet())[0],
+ tuple([list(n.glyphSet())[0] for n in new]))
# GSUB lookup type 4: Ligature substitution.
# Format: "substitute f f i by f_f_i;"
if (not reverse and len(old_prefix) == 0 and len(old_suffix) == 0 and
- len(old) > 1 and len(new) == 1 and len(new[0]) == 1 and
+ len(old) > 1 and len(new) == 1 and
+ len(new[0].glyphSet()) == 1 and
num_lookups == 0):
- return ast.LigatureSubstStatement(location, old, list(new[0])[0])
+ return ast.LigatureSubstStatement(
+ location, old, list(new[0].glyphSet())[0])
# GSUB lookup type 8: Reverse chaining substitution.
if reverse:
@@ -531,7 +535,7 @@ class Parser(object):
"Reverse chaining substitutions cannot call named lookups",
location)
glyphs = sorted(list(old[0].glyphSet()))
- replacements = sorted(list(new[0]))
+ replacements = sorted(list(new[0].glyphSet()))
if len(replacements) == 1:
replacements = replacements * len(glyphs)
if len(glyphs) != len(replacements):
diff --git a/Lib/fontTools/feaLib/parser_test.py b/Lib/fontTools/feaLib/parser_test.py
index 96c989859..94cf0cd17 100644
--- a/Lib/fontTools/feaLib/parser_test.py
+++ b/Lib/fontTools/feaLib/parser_test.py
@@ -717,39 +717,83 @@ class ParserTest(unittest.TestCase):
'"dflt" is not a valid script tag; use "DFLT" instead',
self.parse, "feature test {script dflt;} test;")
- def test_substitute_single_format_a(self): # GSUB LookupType 1
+ def test_sub_single_format_a(self): # GSUB LookupType 1
doc = self.parse("feature smcp {substitute a by a.sc;} smcp;")
sub = doc.statements[0].statements[0]
self.assertEqual(type(sub), ast.SingleSubstStatement)
+ self.assertEqual(glyphstr(sub.prefix), "")
self.assertEqual(sub.mapping, {"a": "a.sc"})
+ self.assertEqual(glyphstr(sub.suffix), "")
- def test_substitute_single_format_b(self): # GSUB LookupType 1
+ def test_sub_single_format_a_chained(self): # chain to GSUB LookupType 1
+ doc = self.parse("feature test {sub [A a] d' [C] by d.alt;} test;")
+ sub = doc.statements[0].statements[0]
+ self.assertIsInstance(sub, ast.SingleSubstStatement)
+ self.assertEqual(sub.mapping, {"d": "d.alt"})
+ self.assertEqual(glyphstr(sub.prefix), "[A a]")
+ self.assertEqual(glyphstr(sub.suffix), "C")
+
+ def test_sub_single_format_b(self): # GSUB LookupType 1
doc = self.parse(
"feature smcp {"
" substitute [one.fitted one.oldstyle] by one;"
"} smcp;")
sub = doc.statements[0].statements[0]
- self.assertEqual(type(sub), ast.SingleSubstStatement)
+ self.assertIsInstance(sub, ast.SingleSubstStatement)
self.assertEqual(sub.mapping, {
"one.fitted": "one",
"one.oldstyle": "one"
})
+ self.assertEqual(glyphstr(sub.prefix), "")
+ self.assertEqual(glyphstr(sub.suffix), "")
- def test_substitute_single_format_c(self): # GSUB LookupType 1
+ def test_sub_single_format_b_chained(self): # chain to GSUB LookupType 1
+ doc = self.parse(
+ "feature smcp {"
+ " substitute PRE FIX [one.fitted one.oldstyle]' SUF FIX by one;"
+ "} smcp;")
+ sub = doc.statements[0].statements[0]
+ self.assertIsInstance(sub, ast.SingleSubstStatement)
+ self.assertEqual(sub.mapping, {
+ "one.fitted": "one",
+ "one.oldstyle": "one"
+ })
+ self.assertEqual(glyphstr(sub.prefix), "PRE FIX")
+ self.assertEqual(glyphstr(sub.suffix), "SUF FIX")
+
+ def test_sub_single_format_c(self): # GSUB LookupType 1
doc = self.parse(
"feature smcp {"
" substitute [a-d] by [A.sc-D.sc];"
"} smcp;")
sub = doc.statements[0].statements[0]
- self.assertEqual(type(sub), ast.SingleSubstStatement)
+ self.assertIsInstance(sub, ast.SingleSubstStatement)
self.assertEqual(sub.mapping, {
"a": "A.sc",
"b": "B.sc",
"c": "C.sc",
"d": "D.sc"
})
+ self.assertEqual(glyphstr(sub.prefix), "")
+ self.assertEqual(glyphstr(sub.suffix), "")
- def test_substitute_single_format_c_different_num_elements(self):
+ def test_sub_single_format_c_chained(self): # chain to GSUB LookupType 1
+ doc = self.parse(
+ "feature smcp {"
+ " substitute [a-d]' X Y [Z z] by [A.sc-D.sc];"
+ "} smcp;")
+ sub = doc.statements[0].statements[0]
+ self.assertIsInstance(sub, ast.SingleSubstStatement)
+ self.assertEqual(sub.mapping, {
+ "a": "A.sc",
+ "b": "B.sc",
+ "c": "C.sc",
+ "d": "D.sc"
+ })
+ self.assertEqual(glyphstr(sub.prefix), "")
+ self.assertEqual(glyphstr(sub.suffix), "X Y [Z z]")
+
+ def test_sub_single_format_c_different_num_elements(self):
self.assertRaisesRegex(
FeatureLibError,
'Expected a glyph class with 4 elements after "by", '
@@ -789,7 +833,7 @@ class ParserTest(unittest.TestCase):
self.assertEqual(glyphstr(sub.glyphs), "f f i")
self.assertEqual(sub.replacement, "f_f_i")
- def test_substitute_lookups(self):
+ def test_substitute_lookups(self): # GSUB LookupType 6
doc = Parser(self.getpath("spec5fi1.fea")).parse()
[langsys, ligs, sub, feature] = doc.statements
self.assertEqual(feature.statements[0].lookups, [ligs, None, sub])
diff --git a/Lib/fontTools/feaLib/testdata/spec5fi2.fea b/Lib/fontTools/feaLib/testdata/spec5fi2.fea
new file mode 100644
index 000000000..235a1d8e1
--- /dev/null
+++ b/Lib/fontTools/feaLib/testdata/spec5fi2.fea
@@ -0,0 +1,10 @@
+# OpenType Feature File specification, section 5.f.i, example 2
+# "Specifying a Chain Sub rule and marking sub-runs"
+# http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html
+
+languagesystem latn dflt;
+
+feature test {
+ lookupflag 7;
+ substitute [a e n] d' by d.alt;
+} test;
diff --git a/Lib/fontTools/feaLib/testdata/spec5fi2.ttx b/Lib/fontTools/feaLib/testdata/spec5fi2.ttx
new file mode 100644
index 000000000..0bce0907a
--- /dev/null
+++ b/Lib/fontTools/feaLib/testdata/spec5fi2.ttx
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Lib/fontTools/feaLib/testdata/spec5fi3.fea b/Lib/fontTools/feaLib/testdata/spec5fi3.fea
new file mode 100644
index 000000000..f6215b165
--- /dev/null
+++ b/Lib/fontTools/feaLib/testdata/spec5fi3.fea
@@ -0,0 +1,9 @@
+# OpenType Feature File specification, section 5.f.i, example 2
+# "Specifying a Chain Sub rule and marking sub-runs"
+# http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html
+
+languagesystem latn dflt;
+
+feature test {
+ substitute [A-Z] [A.sc-Z.sc]' by [a-z];
+} test;
diff --git a/Lib/fontTools/feaLib/testdata/spec5fi3.ttx b/Lib/fontTools/feaLib/testdata/spec5fi3.ttx
new file mode 100644
index 000000000..3bc260acd
--- /dev/null
+++ b/Lib/fontTools/feaLib/testdata/spec5fi3.ttx
@@ -0,0 +1,139 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+