xref: /aosp_15_r20/external/fonttools/Tests/feaLib/parser_test.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1*e1fe3e4aSElliott Hughes# -*- coding: utf-8 -*-
2*e1fe3e4aSElliott Hughesfrom fontTools.misc.loggingTools import CapturingLogHandler
3*e1fe3e4aSElliott Hughesfrom fontTools.feaLib.error import FeatureLibError
4*e1fe3e4aSElliott Hughesfrom fontTools.feaLib.parser import Parser, SymbolTable
5*e1fe3e4aSElliott Hughesfrom io import StringIO
6*e1fe3e4aSElliott Hughesimport warnings
7*e1fe3e4aSElliott Hughesimport fontTools.feaLib.ast as ast
8*e1fe3e4aSElliott Hughesimport os
9*e1fe3e4aSElliott Hughesimport unittest
10*e1fe3e4aSElliott Hughes
11*e1fe3e4aSElliott Hughes
12*e1fe3e4aSElliott Hughesdef glyphstr(glyphs):
13*e1fe3e4aSElliott Hughes    def f(x):
14*e1fe3e4aSElliott Hughes        if len(x) == 1:
15*e1fe3e4aSElliott Hughes            return list(x)[0]
16*e1fe3e4aSElliott Hughes        else:
17*e1fe3e4aSElliott Hughes            return "[%s]" % " ".join(sorted(list(x)))
18*e1fe3e4aSElliott Hughes
19*e1fe3e4aSElliott Hughes    return " ".join(f(g.glyphSet()) for g in glyphs)
20*e1fe3e4aSElliott Hughes
21*e1fe3e4aSElliott Hughes
22*e1fe3e4aSElliott Hughesdef mapping(s):
23*e1fe3e4aSElliott Hughes    b = []
24*e1fe3e4aSElliott Hughes    for a in s.glyphs:
25*e1fe3e4aSElliott Hughes        b.extend(a.glyphSet())
26*e1fe3e4aSElliott Hughes    c = []
27*e1fe3e4aSElliott Hughes    for a in s.replacements:
28*e1fe3e4aSElliott Hughes        c.extend(a.glyphSet())
29*e1fe3e4aSElliott Hughes    if len(c) == 1:
30*e1fe3e4aSElliott Hughes        c = c * len(b)
31*e1fe3e4aSElliott Hughes    return dict(zip(b, c))
32*e1fe3e4aSElliott Hughes
33*e1fe3e4aSElliott Hughes
34*e1fe3e4aSElliott HughesGLYPHNAMES = (
35*e1fe3e4aSElliott Hughes    (
36*e1fe3e4aSElliott Hughes        """
37*e1fe3e4aSElliott Hughes    .notdef space A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
38*e1fe3e4aSElliott Hughes    A.sc B.sc C.sc D.sc E.sc F.sc G.sc H.sc I.sc J.sc K.sc L.sc M.sc
39*e1fe3e4aSElliott Hughes    N.sc O.sc P.sc Q.sc R.sc S.sc T.sc U.sc V.sc W.sc X.sc Y.sc Z.sc
40*e1fe3e4aSElliott Hughes    A.swash B.swash X.swash Y.swash Z.swash
41*e1fe3e4aSElliott Hughes    a b c d e f g h i j k l m n o p q r s t u v w x y z
42*e1fe3e4aSElliott Hughes    a.sc b.sc c.sc d.sc e.sc f.sc g.sc h.sc i.sc j.sc k.sc l.sc m.sc
43*e1fe3e4aSElliott Hughes    n.sc o.sc p.sc q.sc r.sc s.sc t.sc u.sc v.sc w.sc x.sc y.sc z.sc
44*e1fe3e4aSElliott Hughes    a.swash b.swash x.swash y.swash z.swash
45*e1fe3e4aSElliott Hughes    foobar foo.09 foo.1234 foo.9876
46*e1fe3e4aSElliott Hughes    one two five six acute grave dieresis umlaut cedilla ogonek macron
47*e1fe3e4aSElliott Hughes    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
48*e1fe3e4aSElliott Hughes    PRE SUF FIX BACK TRACK LOOK AHEAD ampersand ampersand.1 ampersand.2
49*e1fe3e4aSElliott Hughes    cid00001 cid00002 cid00003 cid00004 cid00005 cid00006 cid00007
50*e1fe3e4aSElliott Hughes    cid12345 cid78987 cid00999 cid01000 cid01001 cid00998 cid00995
51*e1fe3e4aSElliott Hughes    cid00111 cid00222
52*e1fe3e4aSElliott Hughes    comma endash emdash figuredash damma hamza
53*e1fe3e4aSElliott Hughes    c_d d.alt n.end s.end f_f
54*e1fe3e4aSElliott Hughes"""
55*e1fe3e4aSElliott Hughes    ).split()
56*e1fe3e4aSElliott Hughes    + ["foo.%d" % i for i in range(1, 200)]
57*e1fe3e4aSElliott Hughes    + ["G" * 600]
58*e1fe3e4aSElliott Hughes)
59*e1fe3e4aSElliott Hughes
60*e1fe3e4aSElliott Hughes
61*e1fe3e4aSElliott Hughesclass ParserTest(unittest.TestCase):
62*e1fe3e4aSElliott Hughes    def __init__(self, methodName):
63*e1fe3e4aSElliott Hughes        unittest.TestCase.__init__(self, methodName)
64*e1fe3e4aSElliott Hughes        # Python 3 renamed assertRaisesRegexp to assertRaisesRegex,
65*e1fe3e4aSElliott Hughes        # and fires deprecation warnings if a program uses the old name.
66*e1fe3e4aSElliott Hughes        if not hasattr(self, "assertRaisesRegex"):
67*e1fe3e4aSElliott Hughes            self.assertRaisesRegex = self.assertRaisesRegexp
68*e1fe3e4aSElliott Hughes
69*e1fe3e4aSElliott Hughes    def test_glyphMap_deprecated(self):
70*e1fe3e4aSElliott Hughes        glyphMap = {"a": 0, "b": 1, "c": 2}
71*e1fe3e4aSElliott Hughes        with warnings.catch_warnings(record=True) as w:
72*e1fe3e4aSElliott Hughes            warnings.simplefilter("always")
73*e1fe3e4aSElliott Hughes            parser = Parser(StringIO(), glyphMap=glyphMap)
74*e1fe3e4aSElliott Hughes
75*e1fe3e4aSElliott Hughes            self.assertEqual(len(w), 1)
76*e1fe3e4aSElliott Hughes            self.assertEqual(w[-1].category, UserWarning)
77*e1fe3e4aSElliott Hughes            self.assertIn("deprecated", str(w[-1].message))
78*e1fe3e4aSElliott Hughes            self.assertEqual(parser.glyphNames_, {"a", "b", "c"})
79*e1fe3e4aSElliott Hughes
80*e1fe3e4aSElliott Hughes            self.assertRaisesRegex(
81*e1fe3e4aSElliott Hughes                TypeError,
82*e1fe3e4aSElliott Hughes                "mutually exclusive",
83*e1fe3e4aSElliott Hughes                Parser,
84*e1fe3e4aSElliott Hughes                StringIO(),
85*e1fe3e4aSElliott Hughes                ("a",),
86*e1fe3e4aSElliott Hughes                glyphMap={"a": 0},
87*e1fe3e4aSElliott Hughes            )
88*e1fe3e4aSElliott Hughes
89*e1fe3e4aSElliott Hughes            self.assertRaisesRegex(
90*e1fe3e4aSElliott Hughes                TypeError, "unsupported keyword argument", Parser, StringIO(), foo="bar"
91*e1fe3e4aSElliott Hughes            )
92*e1fe3e4aSElliott Hughes
93*e1fe3e4aSElliott Hughes    def test_comments(self):
94*e1fe3e4aSElliott Hughes        doc = self.parse(
95*e1fe3e4aSElliott Hughes            """ # Initial
96*e1fe3e4aSElliott Hughes                feature test {
97*e1fe3e4aSElliott Hughes                    sub A by B; # simple
98*e1fe3e4aSElliott Hughes                } test;"""
99*e1fe3e4aSElliott Hughes        )
100*e1fe3e4aSElliott Hughes        c1 = doc.statements[0]
101*e1fe3e4aSElliott Hughes        c2 = doc.statements[1].statements[1]
102*e1fe3e4aSElliott Hughes        self.assertEqual(type(c1), ast.Comment)
103*e1fe3e4aSElliott Hughes        self.assertEqual(c1.text, "# Initial")
104*e1fe3e4aSElliott Hughes        self.assertEqual(str(c1), "# Initial")
105*e1fe3e4aSElliott Hughes        self.assertEqual(type(c2), ast.Comment)
106*e1fe3e4aSElliott Hughes        self.assertEqual(c2.text, "# simple")
107*e1fe3e4aSElliott Hughes        self.assertEqual(doc.statements[1].name, "test")
108*e1fe3e4aSElliott Hughes
109*e1fe3e4aSElliott Hughes    def test_only_comments(self):
110*e1fe3e4aSElliott Hughes        doc = self.parse(
111*e1fe3e4aSElliott Hughes            """\
112*e1fe3e4aSElliott Hughes            # Initial
113*e1fe3e4aSElliott Hughes        """
114*e1fe3e4aSElliott Hughes        )
115*e1fe3e4aSElliott Hughes        c1 = doc.statements[0]
116*e1fe3e4aSElliott Hughes        self.assertEqual(type(c1), ast.Comment)
117*e1fe3e4aSElliott Hughes        self.assertEqual(c1.text, "# Initial")
118*e1fe3e4aSElliott Hughes        self.assertEqual(str(c1), "# Initial")
119*e1fe3e4aSElliott Hughes
120*e1fe3e4aSElliott Hughes    def test_anchor_format_a(self):
121*e1fe3e4aSElliott Hughes        doc = self.parse(
122*e1fe3e4aSElliott Hughes            "feature test {"
123*e1fe3e4aSElliott Hughes            "    pos cursive A <anchor 120 -20> <anchor NULL>;"
124*e1fe3e4aSElliott Hughes            "} test;"
125*e1fe3e4aSElliott Hughes        )
126*e1fe3e4aSElliott Hughes        anchor = doc.statements[0].statements[0].entryAnchor
127*e1fe3e4aSElliott Hughes        self.assertEqual(type(anchor), ast.Anchor)
128*e1fe3e4aSElliott Hughes        self.assertEqual(anchor.x, 120)
129*e1fe3e4aSElliott Hughes        self.assertEqual(anchor.y, -20)
130*e1fe3e4aSElliott Hughes        self.assertIsNone(anchor.contourpoint)
131*e1fe3e4aSElliott Hughes        self.assertIsNone(anchor.xDeviceTable)
132*e1fe3e4aSElliott Hughes        self.assertIsNone(anchor.yDeviceTable)
133*e1fe3e4aSElliott Hughes
134*e1fe3e4aSElliott Hughes    def test_anchor_format_b(self):
135*e1fe3e4aSElliott Hughes        doc = self.parse(
136*e1fe3e4aSElliott Hughes            "feature test {"
137*e1fe3e4aSElliott Hughes            "    pos cursive A <anchor 120 -20 contourpoint 5> <anchor NULL>;"
138*e1fe3e4aSElliott Hughes            "} test;"
139*e1fe3e4aSElliott Hughes        )
140*e1fe3e4aSElliott Hughes        anchor = doc.statements[0].statements[0].entryAnchor
141*e1fe3e4aSElliott Hughes        self.assertEqual(type(anchor), ast.Anchor)
142*e1fe3e4aSElliott Hughes        self.assertEqual(anchor.x, 120)
143*e1fe3e4aSElliott Hughes        self.assertEqual(anchor.y, -20)
144*e1fe3e4aSElliott Hughes        self.assertEqual(anchor.contourpoint, 5)
145*e1fe3e4aSElliott Hughes        self.assertIsNone(anchor.xDeviceTable)
146*e1fe3e4aSElliott Hughes        self.assertIsNone(anchor.yDeviceTable)
147*e1fe3e4aSElliott Hughes
148*e1fe3e4aSElliott Hughes    def test_anchor_format_c(self):
149*e1fe3e4aSElliott Hughes        doc = self.parse(
150*e1fe3e4aSElliott Hughes            "feature test {"
151*e1fe3e4aSElliott Hughes            "    pos cursive A "
152*e1fe3e4aSElliott Hughes            "        <anchor 120 -20 <device 11 111, 12 112> <device NULL>>"
153*e1fe3e4aSElliott Hughes            "        <anchor NULL>;"
154*e1fe3e4aSElliott Hughes            "} test;"
155*e1fe3e4aSElliott Hughes        )
156*e1fe3e4aSElliott Hughes        anchor = doc.statements[0].statements[0].entryAnchor
157*e1fe3e4aSElliott Hughes        self.assertEqual(type(anchor), ast.Anchor)
158*e1fe3e4aSElliott Hughes        self.assertEqual(anchor.x, 120)
159*e1fe3e4aSElliott Hughes        self.assertEqual(anchor.y, -20)
160*e1fe3e4aSElliott Hughes        self.assertIsNone(anchor.contourpoint)
161*e1fe3e4aSElliott Hughes        self.assertEqual(anchor.xDeviceTable, ((11, 111), (12, 112)))
162*e1fe3e4aSElliott Hughes        self.assertIsNone(anchor.yDeviceTable)
163*e1fe3e4aSElliott Hughes
164*e1fe3e4aSElliott Hughes    def test_anchor_format_d(self):
165*e1fe3e4aSElliott Hughes        doc = self.parse(
166*e1fe3e4aSElliott Hughes            "feature test {"
167*e1fe3e4aSElliott Hughes            "    pos cursive A <anchor 120 -20> <anchor NULL>;"
168*e1fe3e4aSElliott Hughes            "} test;"
169*e1fe3e4aSElliott Hughes        )
170*e1fe3e4aSElliott Hughes        anchor = doc.statements[0].statements[0].exitAnchor
171*e1fe3e4aSElliott Hughes        self.assertIsNone(anchor)
172*e1fe3e4aSElliott Hughes
173*e1fe3e4aSElliott Hughes    def test_anchor_format_e(self):
174*e1fe3e4aSElliott Hughes        doc = self.parse(
175*e1fe3e4aSElliott Hughes            "feature test {"
176*e1fe3e4aSElliott Hughes            "    anchorDef 120 -20 contourpoint 7 Foo;"
177*e1fe3e4aSElliott Hughes            "    pos cursive A <anchor Foo> <anchor NULL>;"
178*e1fe3e4aSElliott Hughes            "} test;"
179*e1fe3e4aSElliott Hughes        )
180*e1fe3e4aSElliott Hughes        anchor = doc.statements[0].statements[1].entryAnchor
181*e1fe3e4aSElliott Hughes        self.assertEqual(type(anchor), ast.Anchor)
182*e1fe3e4aSElliott Hughes        self.assertEqual(anchor.x, 120)
183*e1fe3e4aSElliott Hughes        self.assertEqual(anchor.y, -20)
184*e1fe3e4aSElliott Hughes        self.assertEqual(anchor.contourpoint, 7)
185*e1fe3e4aSElliott Hughes        self.assertIsNone(anchor.xDeviceTable)
186*e1fe3e4aSElliott Hughes        self.assertIsNone(anchor.yDeviceTable)
187*e1fe3e4aSElliott Hughes
188*e1fe3e4aSElliott Hughes    def test_anchor_format_e_undefined(self):
189*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
190*e1fe3e4aSElliott Hughes            FeatureLibError,
191*e1fe3e4aSElliott Hughes            'Unknown anchor "UnknownName"',
192*e1fe3e4aSElliott Hughes            self.parse,
193*e1fe3e4aSElliott Hughes            "feature test {"
194*e1fe3e4aSElliott Hughes            "    position cursive A <anchor UnknownName> <anchor NULL>;"
195*e1fe3e4aSElliott Hughes            "} test;",
196*e1fe3e4aSElliott Hughes        )
197*e1fe3e4aSElliott Hughes
198*e1fe3e4aSElliott Hughes    def test_anchor_variable_scalar(self):
199*e1fe3e4aSElliott Hughes        doc = self.parse(
200*e1fe3e4aSElliott Hughes            "feature test {"
201*e1fe3e4aSElliott Hughes            "    pos cursive A <anchor (wght=200:-100 wght=900:-150 wdth=150,wght=900:-120) -20> <anchor NULL>;"
202*e1fe3e4aSElliott Hughes            "} test;"
203*e1fe3e4aSElliott Hughes        )
204*e1fe3e4aSElliott Hughes        anchor = doc.statements[0].statements[0].entryAnchor
205*e1fe3e4aSElliott Hughes        self.assertEqual(
206*e1fe3e4aSElliott Hughes            anchor.asFea(),
207*e1fe3e4aSElliott Hughes            "<anchor (wght=200:-100 wght=900:-150 wdth=150,wght=900:-120) -20>",
208*e1fe3e4aSElliott Hughes        )
209*e1fe3e4aSElliott Hughes
210*e1fe3e4aSElliott Hughes    def test_anchordef(self):
211*e1fe3e4aSElliott Hughes        [foo] = self.parse("anchorDef 123 456 foo;").statements
212*e1fe3e4aSElliott Hughes        self.assertEqual(type(foo), ast.AnchorDefinition)
213*e1fe3e4aSElliott Hughes        self.assertEqual(foo.name, "foo")
214*e1fe3e4aSElliott Hughes        self.assertEqual(foo.x, 123)
215*e1fe3e4aSElliott Hughes        self.assertEqual(foo.y, 456)
216*e1fe3e4aSElliott Hughes        self.assertEqual(foo.contourpoint, None)
217*e1fe3e4aSElliott Hughes
218*e1fe3e4aSElliott Hughes    def test_anchordef_contourpoint(self):
219*e1fe3e4aSElliott Hughes        [foo] = self.parse("anchorDef 123 456 contourpoint 5 foo;").statements
220*e1fe3e4aSElliott Hughes        self.assertEqual(type(foo), ast.AnchorDefinition)
221*e1fe3e4aSElliott Hughes        self.assertEqual(foo.name, "foo")
222*e1fe3e4aSElliott Hughes        self.assertEqual(foo.x, 123)
223*e1fe3e4aSElliott Hughes        self.assertEqual(foo.y, 456)
224*e1fe3e4aSElliott Hughes        self.assertEqual(foo.contourpoint, 5)
225*e1fe3e4aSElliott Hughes
226*e1fe3e4aSElliott Hughes    def test_anon(self):
227*e1fe3e4aSElliott Hughes        anon = self.parse("anon TEST { # a\nfoo\n } TEST; # qux").statements[0]
228*e1fe3e4aSElliott Hughes        self.assertIsInstance(anon, ast.AnonymousBlock)
229*e1fe3e4aSElliott Hughes        self.assertEqual(anon.tag, "TEST")
230*e1fe3e4aSElliott Hughes        self.assertEqual(anon.content, "foo\n ")
231*e1fe3e4aSElliott Hughes
232*e1fe3e4aSElliott Hughes    def test_anonymous(self):
233*e1fe3e4aSElliott Hughes        anon = self.parse("anonymous TEST {\nbar\n} TEST;").statements[0]
234*e1fe3e4aSElliott Hughes        self.assertIsInstance(anon, ast.AnonymousBlock)
235*e1fe3e4aSElliott Hughes        self.assertEqual(anon.tag, "TEST")
236*e1fe3e4aSElliott Hughes        # feature file spec requires passing the final end-of-line
237*e1fe3e4aSElliott Hughes        self.assertEqual(anon.content, "bar\n")
238*e1fe3e4aSElliott Hughes
239*e1fe3e4aSElliott Hughes    def test_anon_missingBrace(self):
240*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
241*e1fe3e4aSElliott Hughes            FeatureLibError,
242*e1fe3e4aSElliott Hughes            "Expected '} TEST;' to terminate anonymous block",
243*e1fe3e4aSElliott Hughes            self.parse,
244*e1fe3e4aSElliott Hughes            "anon TEST { \n no end in sight",
245*e1fe3e4aSElliott Hughes        )
246*e1fe3e4aSElliott Hughes
247*e1fe3e4aSElliott Hughes    def test_attach(self):
248*e1fe3e4aSElliott Hughes        doc = self.parse("table GDEF {Attach [a e] 2;} GDEF;")
249*e1fe3e4aSElliott Hughes        s = doc.statements[0].statements[0]
250*e1fe3e4aSElliott Hughes        self.assertIsInstance(s, ast.AttachStatement)
251*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([s.glyphs]), "[a e]")
252*e1fe3e4aSElliott Hughes        self.assertEqual(s.contourPoints, {2})
253*e1fe3e4aSElliott Hughes
254*e1fe3e4aSElliott Hughes    def test_feature_block(self):
255*e1fe3e4aSElliott Hughes        [liga] = self.parse("feature liga {} liga;").statements
256*e1fe3e4aSElliott Hughes        self.assertEqual(liga.name, "liga")
257*e1fe3e4aSElliott Hughes        self.assertFalse(liga.use_extension)
258*e1fe3e4aSElliott Hughes
259*e1fe3e4aSElliott Hughes    def test_feature_block_useExtension(self):
260*e1fe3e4aSElliott Hughes        [liga] = self.parse("feature liga useExtension {} liga;").statements
261*e1fe3e4aSElliott Hughes        self.assertEqual(liga.name, "liga")
262*e1fe3e4aSElliott Hughes        self.assertTrue(liga.use_extension)
263*e1fe3e4aSElliott Hughes        self.assertEqual(liga.asFea(), "feature liga useExtension {\n    \n} liga;\n")
264*e1fe3e4aSElliott Hughes
265*e1fe3e4aSElliott Hughes    def test_feature_comment(self):
266*e1fe3e4aSElliott Hughes        [liga] = self.parse("feature liga { # Comment\n } liga;").statements
267*e1fe3e4aSElliott Hughes        [comment] = liga.statements
268*e1fe3e4aSElliott Hughes        self.assertIsInstance(comment, ast.Comment)
269*e1fe3e4aSElliott Hughes        self.assertEqual(comment.text, "# Comment")
270*e1fe3e4aSElliott Hughes
271*e1fe3e4aSElliott Hughes    def test_feature_reference(self):
272*e1fe3e4aSElliott Hughes        doc = self.parse("feature aalt { feature salt; } aalt;")
273*e1fe3e4aSElliott Hughes        ref = doc.statements[0].statements[0]
274*e1fe3e4aSElliott Hughes        self.assertIsInstance(ref, ast.FeatureReferenceStatement)
275*e1fe3e4aSElliott Hughes        self.assertEqual(ref.featureName, "salt")
276*e1fe3e4aSElliott Hughes
277*e1fe3e4aSElliott Hughes    def test_FeatureNames_bad(self):
278*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
279*e1fe3e4aSElliott Hughes            FeatureLibError,
280*e1fe3e4aSElliott Hughes            'Expected "name"',
281*e1fe3e4aSElliott Hughes            self.parse,
282*e1fe3e4aSElliott Hughes            "feature ss01 { featureNames { feature test; } ss01;",
283*e1fe3e4aSElliott Hughes        )
284*e1fe3e4aSElliott Hughes
285*e1fe3e4aSElliott Hughes    def test_FeatureNames_comment(self):
286*e1fe3e4aSElliott Hughes        [feature] = self.parse(
287*e1fe3e4aSElliott Hughes            "feature ss01 { featureNames { # Comment\n }; } ss01;"
288*e1fe3e4aSElliott Hughes        ).statements
289*e1fe3e4aSElliott Hughes        [featureNames] = feature.statements
290*e1fe3e4aSElliott Hughes        self.assertIsInstance(featureNames, ast.NestedBlock)
291*e1fe3e4aSElliott Hughes        [comment] = featureNames.statements
292*e1fe3e4aSElliott Hughes        self.assertIsInstance(comment, ast.Comment)
293*e1fe3e4aSElliott Hughes        self.assertEqual(comment.text, "# Comment")
294*e1fe3e4aSElliott Hughes
295*e1fe3e4aSElliott Hughes    def test_FeatureNames_emptyStatements(self):
296*e1fe3e4aSElliott Hughes        [feature] = self.parse(
297*e1fe3e4aSElliott Hughes            "feature ss01 { featureNames { ;;; }; } ss01;"
298*e1fe3e4aSElliott Hughes        ).statements
299*e1fe3e4aSElliott Hughes        [featureNames] = feature.statements
300*e1fe3e4aSElliott Hughes        self.assertIsInstance(featureNames, ast.NestedBlock)
301*e1fe3e4aSElliott Hughes        self.assertEqual(featureNames.statements, [])
302*e1fe3e4aSElliott Hughes
303*e1fe3e4aSElliott Hughes    def test_FontRevision(self):
304*e1fe3e4aSElliott Hughes        doc = self.parse("table head {FontRevision 2.5;} head;")
305*e1fe3e4aSElliott Hughes        s = doc.statements[0].statements[0]
306*e1fe3e4aSElliott Hughes        self.assertIsInstance(s, ast.FontRevisionStatement)
307*e1fe3e4aSElliott Hughes        self.assertEqual(s.revision, 2.5)
308*e1fe3e4aSElliott Hughes
309*e1fe3e4aSElliott Hughes    def test_FontRevision_negative(self):
310*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
311*e1fe3e4aSElliott Hughes            FeatureLibError,
312*e1fe3e4aSElliott Hughes            "Font revision numbers must be positive",
313*e1fe3e4aSElliott Hughes            self.parse,
314*e1fe3e4aSElliott Hughes            "table head {FontRevision -17.2;} head;",
315*e1fe3e4aSElliott Hughes        )
316*e1fe3e4aSElliott Hughes
317*e1fe3e4aSElliott Hughes    def test_strict_glyph_name_check(self):
318*e1fe3e4aSElliott Hughes        self.parse("@bad = [a b ccc];", glyphNames=("a", "b", "ccc"))
319*e1fe3e4aSElliott Hughes
320*e1fe3e4aSElliott Hughes        with self.assertRaisesRegex(
321*e1fe3e4aSElliott Hughes            FeatureLibError, "(?s)missing from the glyph set:.*ccc"
322*e1fe3e4aSElliott Hughes        ):
323*e1fe3e4aSElliott Hughes            self.parse("@bad = [a b ccc];", glyphNames=("a", "b"))
324*e1fe3e4aSElliott Hughes
325*e1fe3e4aSElliott Hughes    def test_glyphclass(self):
326*e1fe3e4aSElliott Hughes        [gc] = self.parse("@dash = [endash emdash figuredash];").statements
327*e1fe3e4aSElliott Hughes        self.assertEqual(gc.name, "dash")
328*e1fe3e4aSElliott Hughes        self.assertEqual(gc.glyphSet(), ("endash", "emdash", "figuredash"))
329*e1fe3e4aSElliott Hughes
330*e1fe3e4aSElliott Hughes    def test_glyphclass_glyphNameTooLong(self):
331*e1fe3e4aSElliott Hughes        gname = "G" * 600
332*e1fe3e4aSElliott Hughes        [gc] = self.parse(f"@GlyphClass = [{gname}];").statements
333*e1fe3e4aSElliott Hughes        self.assertEqual(gc.name, "GlyphClass")
334*e1fe3e4aSElliott Hughes        self.assertEqual(gc.glyphSet(), (gname,))
335*e1fe3e4aSElliott Hughes
336*e1fe3e4aSElliott Hughes    def test_glyphclass_bad(self):
337*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
338*e1fe3e4aSElliott Hughes            FeatureLibError,
339*e1fe3e4aSElliott Hughes            "Expected glyph name, glyph range, or glyph class reference",
340*e1fe3e4aSElliott Hughes            self.parse,
341*e1fe3e4aSElliott Hughes            "@bad = [a 123];",
342*e1fe3e4aSElliott Hughes        )
343*e1fe3e4aSElliott Hughes
344*e1fe3e4aSElliott Hughes    def test_glyphclass_duplicate(self):
345*e1fe3e4aSElliott Hughes        # makeotf accepts this, so we should too
346*e1fe3e4aSElliott Hughes        ab, xy = self.parse("@dup = [a b]; @dup = [x y];").statements
347*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([ab]), "[a b]")
348*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([xy]), "[x y]")
349*e1fe3e4aSElliott Hughes
350*e1fe3e4aSElliott Hughes    def test_glyphclass_empty(self):
351*e1fe3e4aSElliott Hughes        [gc] = self.parse("@empty_set = [];").statements
352*e1fe3e4aSElliott Hughes        self.assertEqual(gc.name, "empty_set")
353*e1fe3e4aSElliott Hughes        self.assertEqual(gc.glyphSet(), tuple())
354*e1fe3e4aSElliott Hughes
355*e1fe3e4aSElliott Hughes    def test_glyphclass_equality(self):
356*e1fe3e4aSElliott Hughes        [foo, bar] = self.parse("@foo = [a b]; @bar = @foo;").statements
357*e1fe3e4aSElliott Hughes        self.assertEqual(foo.glyphSet(), ("a", "b"))
358*e1fe3e4aSElliott Hughes        self.assertEqual(bar.glyphSet(), ("a", "b"))
359*e1fe3e4aSElliott Hughes
360*e1fe3e4aSElliott Hughes    def test_glyphclass_from_markClass(self):
361*e1fe3e4aSElliott Hughes        doc = self.parse(
362*e1fe3e4aSElliott Hughes            "markClass [acute grave] <anchor 500 800> @TOP_MARKS;"
363*e1fe3e4aSElliott Hughes            "markClass cedilla <anchor 500 -100> @BOTTOM_MARKS;"
364*e1fe3e4aSElliott Hughes            "@MARKS = [@TOP_MARKS @BOTTOM_MARKS ogonek];"
365*e1fe3e4aSElliott Hughes            "@ALL = @MARKS;"
366*e1fe3e4aSElliott Hughes        )
367*e1fe3e4aSElliott Hughes        self.assertEqual(
368*e1fe3e4aSElliott Hughes            doc.statements[-1].glyphSet(), ("acute", "grave", "cedilla", "ogonek")
369*e1fe3e4aSElliott Hughes        )
370*e1fe3e4aSElliott Hughes
371*e1fe3e4aSElliott Hughes    def test_glyphclass_range_cid(self):
372*e1fe3e4aSElliott Hughes        [gc] = self.parse(r"@GlyphClass = [\999-\1001];").statements
373*e1fe3e4aSElliott Hughes        self.assertEqual(gc.name, "GlyphClass")
374*e1fe3e4aSElliott Hughes        self.assertEqual(gc.glyphSet(), ("cid00999", "cid01000", "cid01001"))
375*e1fe3e4aSElliott Hughes
376*e1fe3e4aSElliott Hughes    def test_glyphclass_range_cid_bad(self):
377*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
378*e1fe3e4aSElliott Hughes            FeatureLibError,
379*e1fe3e4aSElliott Hughes            "Bad range: start should be less than limit",
380*e1fe3e4aSElliott Hughes            self.parse,
381*e1fe3e4aSElliott Hughes            r"@bad = [\998-\995];",
382*e1fe3e4aSElliott Hughes        )
383*e1fe3e4aSElliott Hughes
384*e1fe3e4aSElliott Hughes    def test_glyphclass_range_uppercase(self):
385*e1fe3e4aSElliott Hughes        [gc] = self.parse("@swashes = [X.swash-Z.swash];").statements
386*e1fe3e4aSElliott Hughes        self.assertEqual(gc.name, "swashes")
387*e1fe3e4aSElliott Hughes        self.assertEqual(gc.glyphSet(), ("X.swash", "Y.swash", "Z.swash"))
388*e1fe3e4aSElliott Hughes
389*e1fe3e4aSElliott Hughes    def test_glyphclass_range_lowercase(self):
390*e1fe3e4aSElliott Hughes        [gc] = self.parse("@defg.sc = [d.sc-g.sc];").statements
391*e1fe3e4aSElliott Hughes        self.assertEqual(gc.name, "defg.sc")
392*e1fe3e4aSElliott Hughes        self.assertEqual(gc.glyphSet(), ("d.sc", "e.sc", "f.sc", "g.sc"))
393*e1fe3e4aSElliott Hughes
394*e1fe3e4aSElliott Hughes    def test_glyphclass_range_dash(self):
395*e1fe3e4aSElliott Hughes        glyphNames = "A-foo.sc B-foo.sc C-foo.sc".split()
396*e1fe3e4aSElliott Hughes        [gc] = self.parse("@range = [A-foo.sc-C-foo.sc];", glyphNames).statements
397*e1fe3e4aSElliott Hughes        self.assertEqual(gc.glyphSet(), ("A-foo.sc", "B-foo.sc", "C-foo.sc"))
398*e1fe3e4aSElliott Hughes
399*e1fe3e4aSElliott Hughes    def test_glyphclass_range_dash_with_space(self):
400*e1fe3e4aSElliott Hughes        gn = "A-foo.sc B-foo.sc C-foo.sc".split()
401*e1fe3e4aSElliott Hughes        [gc] = self.parse("@range = [A-foo.sc - C-foo.sc];", gn).statements
402*e1fe3e4aSElliott Hughes        self.assertEqual(gc.glyphSet(), ("A-foo.sc", "B-foo.sc", "C-foo.sc"))
403*e1fe3e4aSElliott Hughes
404*e1fe3e4aSElliott Hughes    def test_glyphclass_ambiguous_dash_no_glyph_names(self):
405*e1fe3e4aSElliott Hughes        # If Parser is initialized without a glyphNames parameter (or with empty one)
406*e1fe3e4aSElliott Hughes        # it cannot distinguish between a glyph name containing an hyphen, or a
407*e1fe3e4aSElliott Hughes        # range of glyph names; thus it will interpret them as literal glyph names
408*e1fe3e4aSElliott Hughes        # while also outputting a logging warning to alert user about the ambiguity.
409*e1fe3e4aSElliott Hughes        # https://github.com/fonttools/fonttools/issues/1768
410*e1fe3e4aSElliott Hughes        glyphNames = ()
411*e1fe3e4aSElliott Hughes        with CapturingLogHandler("fontTools.feaLib.parser", level="WARNING") as caplog:
412*e1fe3e4aSElliott Hughes            [gc] = self.parse(
413*e1fe3e4aSElliott Hughes                "@class = [A-foo.sc B-foo.sc C D];", glyphNames
414*e1fe3e4aSElliott Hughes            ).statements
415*e1fe3e4aSElliott Hughes        self.assertEqual(gc.glyphSet(), ("A-foo.sc", "B-foo.sc", "C", "D"))
416*e1fe3e4aSElliott Hughes        self.assertEqual(len(caplog.records), 2)
417*e1fe3e4aSElliott Hughes        caplog.assertRegex("Ambiguous glyph name that looks like a range:")
418*e1fe3e4aSElliott Hughes
419*e1fe3e4aSElliott Hughes    def test_glyphclass_glyph_name_should_win_over_range(self):
420*e1fe3e4aSElliott Hughes        # The OpenType Feature File Specification v1.20 makes it clear
421*e1fe3e4aSElliott Hughes        # that if a dashed name could be interpreted either as a glyph name
422*e1fe3e4aSElliott Hughes        # or as a range, then the semantics should be the single dashed name.
423*e1fe3e4aSElliott Hughes        glyphNames = "A-foo.sc-C-foo.sc A-foo.sc B-foo.sc C-foo.sc".split()
424*e1fe3e4aSElliott Hughes        [gc] = self.parse("@range = [A-foo.sc-C-foo.sc];", glyphNames).statements
425*e1fe3e4aSElliott Hughes        self.assertEqual(gc.glyphSet(), ("A-foo.sc-C-foo.sc",))
426*e1fe3e4aSElliott Hughes
427*e1fe3e4aSElliott Hughes    def test_glyphclass_range_dash_ambiguous(self):
428*e1fe3e4aSElliott Hughes        glyphNames = "A B C A-B B-C".split()
429*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
430*e1fe3e4aSElliott Hughes            FeatureLibError,
431*e1fe3e4aSElliott Hughes            'Ambiguous glyph range "A-B-C"; '
432*e1fe3e4aSElliott Hughes            'please use "A - B-C" or "A-B - C" to clarify what you mean',
433*e1fe3e4aSElliott Hughes            self.parse,
434*e1fe3e4aSElliott Hughes            r"@bad = [A-B-C];",
435*e1fe3e4aSElliott Hughes            glyphNames,
436*e1fe3e4aSElliott Hughes        )
437*e1fe3e4aSElliott Hughes
438*e1fe3e4aSElliott Hughes    def test_glyphclass_range_digit1(self):
439*e1fe3e4aSElliott Hughes        [gc] = self.parse("@range = [foo.2-foo.5];").statements
440*e1fe3e4aSElliott Hughes        self.assertEqual(gc.glyphSet(), ("foo.2", "foo.3", "foo.4", "foo.5"))
441*e1fe3e4aSElliott Hughes
442*e1fe3e4aSElliott Hughes    def test_glyphclass_range_digit2(self):
443*e1fe3e4aSElliott Hughes        [gc] = self.parse("@range = [foo.09-foo.11];").statements
444*e1fe3e4aSElliott Hughes        self.assertEqual(gc.glyphSet(), ("foo.09", "foo.10", "foo.11"))
445*e1fe3e4aSElliott Hughes
446*e1fe3e4aSElliott Hughes    def test_glyphclass_range_digit3(self):
447*e1fe3e4aSElliott Hughes        [gc] = self.parse("@range = [foo.123-foo.125];").statements
448*e1fe3e4aSElliott Hughes        self.assertEqual(gc.glyphSet(), ("foo.123", "foo.124", "foo.125"))
449*e1fe3e4aSElliott Hughes
450*e1fe3e4aSElliott Hughes    def test_glyphclass_range_bad(self):
451*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
452*e1fe3e4aSElliott Hughes            FeatureLibError,
453*e1fe3e4aSElliott Hughes            'Bad range: "a" and "foobar" should have the same length',
454*e1fe3e4aSElliott Hughes            self.parse,
455*e1fe3e4aSElliott Hughes            "@bad = [a-foobar];",
456*e1fe3e4aSElliott Hughes        )
457*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
458*e1fe3e4aSElliott Hughes            FeatureLibError,
459*e1fe3e4aSElliott Hughes            'Bad range: "A.swash-z.swash"',
460*e1fe3e4aSElliott Hughes            self.parse,
461*e1fe3e4aSElliott Hughes            "@bad = [A.swash-z.swash];",
462*e1fe3e4aSElliott Hughes        )
463*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
464*e1fe3e4aSElliott Hughes            FeatureLibError,
465*e1fe3e4aSElliott Hughes            "Start of range must be smaller than its end",
466*e1fe3e4aSElliott Hughes            self.parse,
467*e1fe3e4aSElliott Hughes            "@bad = [B.swash-A.swash];",
468*e1fe3e4aSElliott Hughes        )
469*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
470*e1fe3e4aSElliott Hughes            FeatureLibError,
471*e1fe3e4aSElliott Hughes            'Bad range: "foo.1234-foo.9876"',
472*e1fe3e4aSElliott Hughes            self.parse,
473*e1fe3e4aSElliott Hughes            "@bad = [foo.1234-foo.9876];",
474*e1fe3e4aSElliott Hughes        )
475*e1fe3e4aSElliott Hughes
476*e1fe3e4aSElliott Hughes    def test_glyphclass_range_mixed(self):
477*e1fe3e4aSElliott Hughes        [gc] = self.parse("@range = [a foo.09-foo.11 X.sc-Z.sc];").statements
478*e1fe3e4aSElliott Hughes        self.assertEqual(
479*e1fe3e4aSElliott Hughes            gc.glyphSet(), ("a", "foo.09", "foo.10", "foo.11", "X.sc", "Y.sc", "Z.sc")
480*e1fe3e4aSElliott Hughes        )
481*e1fe3e4aSElliott Hughes
482*e1fe3e4aSElliott Hughes    def test_glyphclass_reference(self):
483*e1fe3e4aSElliott Hughes        [vowels_lc, vowels_uc, vowels] = self.parse(
484*e1fe3e4aSElliott Hughes            "@Vowels.lc = [a e i o u]; @Vowels.uc = [A E I O U];"
485*e1fe3e4aSElliott Hughes            "@Vowels = [@Vowels.lc @Vowels.uc y Y];"
486*e1fe3e4aSElliott Hughes        ).statements
487*e1fe3e4aSElliott Hughes        self.assertEqual(vowels_lc.glyphSet(), tuple("aeiou"))
488*e1fe3e4aSElliott Hughes        self.assertEqual(vowels_uc.glyphSet(), tuple("AEIOU"))
489*e1fe3e4aSElliott Hughes        self.assertEqual(vowels.glyphSet(), tuple("aeiouAEIOUyY"))
490*e1fe3e4aSElliott Hughes        self.assertEqual(vowels.asFea(), "@Vowels = [@Vowels.lc @Vowels.uc y Y];")
491*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
492*e1fe3e4aSElliott Hughes            FeatureLibError,
493*e1fe3e4aSElliott Hughes            "Unknown glyph class @unknown",
494*e1fe3e4aSElliott Hughes            self.parse,
495*e1fe3e4aSElliott Hughes            "@bad = [@unknown];",
496*e1fe3e4aSElliott Hughes        )
497*e1fe3e4aSElliott Hughes
498*e1fe3e4aSElliott Hughes    def test_glyphclass_scoping(self):
499*e1fe3e4aSElliott Hughes        [foo, liga, smcp] = self.parse(
500*e1fe3e4aSElliott Hughes            "@foo = [a b];"
501*e1fe3e4aSElliott Hughes            "feature liga { @bar = [@foo l]; } liga;"
502*e1fe3e4aSElliott Hughes            "feature smcp { @bar = [@foo s]; } smcp;"
503*e1fe3e4aSElliott Hughes        ).statements
504*e1fe3e4aSElliott Hughes        self.assertEqual(foo.glyphSet(), ("a", "b"))
505*e1fe3e4aSElliott Hughes        self.assertEqual(liga.statements[0].glyphSet(), ("a", "b", "l"))
506*e1fe3e4aSElliott Hughes        self.assertEqual(smcp.statements[0].glyphSet(), ("a", "b", "s"))
507*e1fe3e4aSElliott Hughes
508*e1fe3e4aSElliott Hughes    def test_glyphclass_scoping_bug496(self):
509*e1fe3e4aSElliott Hughes        # https://github.com/fonttools/fonttools/issues/496
510*e1fe3e4aSElliott Hughes        f1, f2 = self.parse(
511*e1fe3e4aSElliott Hughes            "feature F1 { lookup L { @GLYPHCLASS = [A B C];} L; } F1;"
512*e1fe3e4aSElliott Hughes            "feature F2 { sub @GLYPHCLASS by D; } F2;"
513*e1fe3e4aSElliott Hughes        ).statements
514*e1fe3e4aSElliott Hughes        self.assertEqual(list(f2.statements[0].glyphs[0].glyphSet()), ["A", "B", "C"])
515*e1fe3e4aSElliott Hughes
516*e1fe3e4aSElliott Hughes    def test_GlyphClassDef(self):
517*e1fe3e4aSElliott Hughes        doc = self.parse("table GDEF {GlyphClassDef [b],[l],[m],[C c];} GDEF;")
518*e1fe3e4aSElliott Hughes        s = doc.statements[0].statements[0]
519*e1fe3e4aSElliott Hughes        self.assertIsInstance(s, ast.GlyphClassDefStatement)
520*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([s.baseGlyphs]), "b")
521*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([s.ligatureGlyphs]), "l")
522*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([s.markGlyphs]), "m")
523*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([s.componentGlyphs]), "[C c]")
524*e1fe3e4aSElliott Hughes
525*e1fe3e4aSElliott Hughes    def test_GlyphClassDef_noCLassesSpecified(self):
526*e1fe3e4aSElliott Hughes        doc = self.parse("table GDEF {GlyphClassDef ,,,;} GDEF;")
527*e1fe3e4aSElliott Hughes        s = doc.statements[0].statements[0]
528*e1fe3e4aSElliott Hughes        self.assertIsNone(s.baseGlyphs)
529*e1fe3e4aSElliott Hughes        self.assertIsNone(s.ligatureGlyphs)
530*e1fe3e4aSElliott Hughes        self.assertIsNone(s.markGlyphs)
531*e1fe3e4aSElliott Hughes        self.assertIsNone(s.componentGlyphs)
532*e1fe3e4aSElliott Hughes
533*e1fe3e4aSElliott Hughes    def test_ignore_pos(self):
534*e1fe3e4aSElliott Hughes        doc = self.parse("feature test {ignore pos e t' c, q u' u' x;} test;")
535*e1fe3e4aSElliott Hughes        sub = doc.statements[0].statements[0]
536*e1fe3e4aSElliott Hughes        self.assertIsInstance(sub, ast.IgnorePosStatement)
537*e1fe3e4aSElliott Hughes        [(pref1, glyphs1, suff1), (pref2, glyphs2, suff2)] = sub.chainContexts
538*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(pref1), "e")
539*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(glyphs1), "t")
540*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(suff1), "c")
541*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(pref2), "q")
542*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(glyphs2), "u u")
543*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(suff2), "x")
544*e1fe3e4aSElliott Hughes
545*e1fe3e4aSElliott Hughes    def test_ignore_position(self):
546*e1fe3e4aSElliott Hughes        doc = self.parse(
547*e1fe3e4aSElliott Hughes            "feature test {" "    ignore position f [a e] d' [a u]' [e y];" "} test;"
548*e1fe3e4aSElliott Hughes        )
549*e1fe3e4aSElliott Hughes        sub = doc.statements[0].statements[0]
550*e1fe3e4aSElliott Hughes        self.assertIsInstance(sub, ast.IgnorePosStatement)
551*e1fe3e4aSElliott Hughes        [(prefix, glyphs, suffix)] = sub.chainContexts
552*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(prefix), "f [a e]")
553*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(glyphs), "d [a u]")
554*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(suffix), "[e y]")
555*e1fe3e4aSElliott Hughes
556*e1fe3e4aSElliott Hughes    def test_ignore_position_with_lookup(self):
557*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
558*e1fe3e4aSElliott Hughes            FeatureLibError,
559*e1fe3e4aSElliott Hughes            'No lookups can be specified for "ignore pos"',
560*e1fe3e4aSElliott Hughes            self.parse,
561*e1fe3e4aSElliott Hughes            "lookup L { pos [A A.sc] -100; } L;"
562*e1fe3e4aSElliott Hughes            "feature test { ignore pos f' i', A' lookup L; } test;",
563*e1fe3e4aSElliott Hughes        )
564*e1fe3e4aSElliott Hughes
565*e1fe3e4aSElliott Hughes    def test_ignore_sub(self):
566*e1fe3e4aSElliott Hughes        doc = self.parse("feature test {ignore sub e t' c, q u' u' x;} test;")
567*e1fe3e4aSElliott Hughes        sub = doc.statements[0].statements[0]
568*e1fe3e4aSElliott Hughes        self.assertIsInstance(sub, ast.IgnoreSubstStatement)
569*e1fe3e4aSElliott Hughes        [(pref1, glyphs1, suff1), (pref2, glyphs2, suff2)] = sub.chainContexts
570*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(pref1), "e")
571*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(glyphs1), "t")
572*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(suff1), "c")
573*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(pref2), "q")
574*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(glyphs2), "u u")
575*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(suff2), "x")
576*e1fe3e4aSElliott Hughes
577*e1fe3e4aSElliott Hughes    def test_ignore_substitute(self):
578*e1fe3e4aSElliott Hughes        doc = self.parse(
579*e1fe3e4aSElliott Hughes            "feature test {" "    ignore substitute f [a e] d' [a u]' [e y];" "} test;"
580*e1fe3e4aSElliott Hughes        )
581*e1fe3e4aSElliott Hughes        sub = doc.statements[0].statements[0]
582*e1fe3e4aSElliott Hughes        self.assertIsInstance(sub, ast.IgnoreSubstStatement)
583*e1fe3e4aSElliott Hughes        [(prefix, glyphs, suffix)] = sub.chainContexts
584*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(prefix), "f [a e]")
585*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(glyphs), "d [a u]")
586*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(suffix), "[e y]")
587*e1fe3e4aSElliott Hughes
588*e1fe3e4aSElliott Hughes    def test_ignore_substitute_with_lookup(self):
589*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
590*e1fe3e4aSElliott Hughes            FeatureLibError,
591*e1fe3e4aSElliott Hughes            'No lookups can be specified for "ignore sub"',
592*e1fe3e4aSElliott Hughes            self.parse,
593*e1fe3e4aSElliott Hughes            "lookup L { sub [A A.sc] by a; } L;"
594*e1fe3e4aSElliott Hughes            "feature test { ignore sub f' i', A' lookup L; } test;",
595*e1fe3e4aSElliott Hughes        )
596*e1fe3e4aSElliott Hughes
597*e1fe3e4aSElliott Hughes    def test_include_statement(self):
598*e1fe3e4aSElliott Hughes        doc = self.parse(
599*e1fe3e4aSElliott Hughes            """\
600*e1fe3e4aSElliott Hughes            include(../family.fea);
601*e1fe3e4aSElliott Hughes            include # Comment
602*e1fe3e4aSElliott Hughes                (foo)
603*e1fe3e4aSElliott Hughes                  ;
604*e1fe3e4aSElliott Hughes            """,
605*e1fe3e4aSElliott Hughes            followIncludes=False,
606*e1fe3e4aSElliott Hughes        )
607*e1fe3e4aSElliott Hughes        s1, s2, s3 = doc.statements
608*e1fe3e4aSElliott Hughes        self.assertEqual(type(s1), ast.IncludeStatement)
609*e1fe3e4aSElliott Hughes        self.assertEqual(s1.filename, "../family.fea")
610*e1fe3e4aSElliott Hughes        self.assertEqual(s1.asFea(), "include(../family.fea);")
611*e1fe3e4aSElliott Hughes        self.assertEqual(type(s2), ast.IncludeStatement)
612*e1fe3e4aSElliott Hughes        self.assertEqual(s2.filename, "foo")
613*e1fe3e4aSElliott Hughes        self.assertEqual(s2.asFea(), "include(foo);")
614*e1fe3e4aSElliott Hughes        self.assertEqual(type(s3), ast.Comment)
615*e1fe3e4aSElliott Hughes        self.assertEqual(s3.text, "# Comment")
616*e1fe3e4aSElliott Hughes
617*e1fe3e4aSElliott Hughes    def test_include_statement_no_semicolon(self):
618*e1fe3e4aSElliott Hughes        doc = self.parse(
619*e1fe3e4aSElliott Hughes            """\
620*e1fe3e4aSElliott Hughes            include(../family.fea)
621*e1fe3e4aSElliott Hughes            """,
622*e1fe3e4aSElliott Hughes            followIncludes=False,
623*e1fe3e4aSElliott Hughes        )
624*e1fe3e4aSElliott Hughes        s1 = doc.statements[0]
625*e1fe3e4aSElliott Hughes        self.assertEqual(type(s1), ast.IncludeStatement)
626*e1fe3e4aSElliott Hughes        self.assertEqual(s1.filename, "../family.fea")
627*e1fe3e4aSElliott Hughes        self.assertEqual(s1.asFea(), "include(../family.fea);")
628*e1fe3e4aSElliott Hughes
629*e1fe3e4aSElliott Hughes    def test_language(self):
630*e1fe3e4aSElliott Hughes        doc = self.parse("feature test {language DEU;} test;")
631*e1fe3e4aSElliott Hughes        s = doc.statements[0].statements[0]
632*e1fe3e4aSElliott Hughes        self.assertEqual(type(s), ast.LanguageStatement)
633*e1fe3e4aSElliott Hughes        self.assertEqual(s.language, "DEU ")
634*e1fe3e4aSElliott Hughes        self.assertTrue(s.include_default)
635*e1fe3e4aSElliott Hughes        self.assertFalse(s.required)
636*e1fe3e4aSElliott Hughes
637*e1fe3e4aSElliott Hughes    def test_language_exclude_dflt(self):
638*e1fe3e4aSElliott Hughes        doc = self.parse("feature test {language DEU exclude_dflt;} test;")
639*e1fe3e4aSElliott Hughes        s = doc.statements[0].statements[0]
640*e1fe3e4aSElliott Hughes        self.assertEqual(type(s), ast.LanguageStatement)
641*e1fe3e4aSElliott Hughes        self.assertEqual(s.language, "DEU ")
642*e1fe3e4aSElliott Hughes        self.assertFalse(s.include_default)
643*e1fe3e4aSElliott Hughes        self.assertFalse(s.required)
644*e1fe3e4aSElliott Hughes
645*e1fe3e4aSElliott Hughes    def test_language_exclude_dflt_required(self):
646*e1fe3e4aSElliott Hughes        doc = self.parse(
647*e1fe3e4aSElliott Hughes            "feature test {" "  language DEU exclude_dflt required;" "} test;"
648*e1fe3e4aSElliott Hughes        )
649*e1fe3e4aSElliott Hughes        s = doc.statements[0].statements[0]
650*e1fe3e4aSElliott Hughes        self.assertEqual(type(s), ast.LanguageStatement)
651*e1fe3e4aSElliott Hughes        self.assertEqual(s.language, "DEU ")
652*e1fe3e4aSElliott Hughes        self.assertFalse(s.include_default)
653*e1fe3e4aSElliott Hughes        self.assertTrue(s.required)
654*e1fe3e4aSElliott Hughes
655*e1fe3e4aSElliott Hughes    def test_language_include_dflt(self):
656*e1fe3e4aSElliott Hughes        doc = self.parse("feature test {language DEU include_dflt;} test;")
657*e1fe3e4aSElliott Hughes        s = doc.statements[0].statements[0]
658*e1fe3e4aSElliott Hughes        self.assertEqual(type(s), ast.LanguageStatement)
659*e1fe3e4aSElliott Hughes        self.assertEqual(s.language, "DEU ")
660*e1fe3e4aSElliott Hughes        self.assertTrue(s.include_default)
661*e1fe3e4aSElliott Hughes        self.assertFalse(s.required)
662*e1fe3e4aSElliott Hughes
663*e1fe3e4aSElliott Hughes    def test_language_include_dflt_required(self):
664*e1fe3e4aSElliott Hughes        doc = self.parse(
665*e1fe3e4aSElliott Hughes            "feature test {" "  language DEU include_dflt required;" "} test;"
666*e1fe3e4aSElliott Hughes        )
667*e1fe3e4aSElliott Hughes        s = doc.statements[0].statements[0]
668*e1fe3e4aSElliott Hughes        self.assertEqual(type(s), ast.LanguageStatement)
669*e1fe3e4aSElliott Hughes        self.assertEqual(s.language, "DEU ")
670*e1fe3e4aSElliott Hughes        self.assertTrue(s.include_default)
671*e1fe3e4aSElliott Hughes        self.assertTrue(s.required)
672*e1fe3e4aSElliott Hughes
673*e1fe3e4aSElliott Hughes    def test_language_DFLT(self):
674*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
675*e1fe3e4aSElliott Hughes            FeatureLibError,
676*e1fe3e4aSElliott Hughes            '"DFLT" is not a valid language tag; use "dflt" instead',
677*e1fe3e4aSElliott Hughes            self.parse,
678*e1fe3e4aSElliott Hughes            "feature test { language DFLT; } test;",
679*e1fe3e4aSElliott Hughes        )
680*e1fe3e4aSElliott Hughes
681*e1fe3e4aSElliott Hughes    def test_ligatureCaretByIndex_glyphClass(self):
682*e1fe3e4aSElliott Hughes        doc = self.parse("table GDEF{LigatureCaretByIndex [c_t f_i] 2;}GDEF;")
683*e1fe3e4aSElliott Hughes        s = doc.statements[0].statements[0]
684*e1fe3e4aSElliott Hughes        self.assertIsInstance(s, ast.LigatureCaretByIndexStatement)
685*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([s.glyphs]), "[c_t f_i]")
686*e1fe3e4aSElliott Hughes        self.assertEqual(s.carets, [2])
687*e1fe3e4aSElliott Hughes
688*e1fe3e4aSElliott Hughes    def test_ligatureCaretByIndex_singleGlyph(self):
689*e1fe3e4aSElliott Hughes        doc = self.parse("table GDEF{LigatureCaretByIndex f_f_i 3 7;}GDEF;")
690*e1fe3e4aSElliott Hughes        s = doc.statements[0].statements[0]
691*e1fe3e4aSElliott Hughes        self.assertIsInstance(s, ast.LigatureCaretByIndexStatement)
692*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([s.glyphs]), "f_f_i")
693*e1fe3e4aSElliott Hughes        self.assertEqual(s.carets, [3, 7])
694*e1fe3e4aSElliott Hughes
695*e1fe3e4aSElliott Hughes    def test_ligatureCaretByPos_glyphClass(self):
696*e1fe3e4aSElliott Hughes        doc = self.parse("table GDEF {LigatureCaretByPos [c_t f_i] 7;} GDEF;")
697*e1fe3e4aSElliott Hughes        s = doc.statements[0].statements[0]
698*e1fe3e4aSElliott Hughes        self.assertIsInstance(s, ast.LigatureCaretByPosStatement)
699*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([s.glyphs]), "[c_t f_i]")
700*e1fe3e4aSElliott Hughes        self.assertEqual(s.carets, [7])
701*e1fe3e4aSElliott Hughes
702*e1fe3e4aSElliott Hughes    def test_ligatureCaretByPos_singleGlyph(self):
703*e1fe3e4aSElliott Hughes        doc = self.parse("table GDEF {LigatureCaretByPos f_i 400 380;} GDEF;")
704*e1fe3e4aSElliott Hughes        s = doc.statements[0].statements[0]
705*e1fe3e4aSElliott Hughes        self.assertIsInstance(s, ast.LigatureCaretByPosStatement)
706*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([s.glyphs]), "f_i")
707*e1fe3e4aSElliott Hughes        self.assertEqual(s.carets, [400, 380])
708*e1fe3e4aSElliott Hughes
709*e1fe3e4aSElliott Hughes    def test_ligatureCaretByPos_variable_scalar(self):
710*e1fe3e4aSElliott Hughes        doc = self.parse(
711*e1fe3e4aSElliott Hughes            "table GDEF {LigatureCaretByPos f_i (wght=200:400 wght=900:1000) 380;} GDEF;"
712*e1fe3e4aSElliott Hughes        )
713*e1fe3e4aSElliott Hughes        s = doc.statements[0].statements[0]
714*e1fe3e4aSElliott Hughes        self.assertIsInstance(s, ast.LigatureCaretByPosStatement)
715*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([s.glyphs]), "f_i")
716*e1fe3e4aSElliott Hughes        self.assertEqual(len(s.carets), 2)
717*e1fe3e4aSElliott Hughes        self.assertEqual(str(s.carets[0]), "(wght=200:400 wght=900:1000)")
718*e1fe3e4aSElliott Hughes        self.assertEqual(s.carets[1], 380)
719*e1fe3e4aSElliott Hughes
720*e1fe3e4aSElliott Hughes    def test_lookup_block(self):
721*e1fe3e4aSElliott Hughes        [lookup] = self.parse("lookup Ligatures {} Ligatures;").statements
722*e1fe3e4aSElliott Hughes        self.assertEqual(lookup.name, "Ligatures")
723*e1fe3e4aSElliott Hughes        self.assertFalse(lookup.use_extension)
724*e1fe3e4aSElliott Hughes
725*e1fe3e4aSElliott Hughes    def test_lookup_block_useExtension(self):
726*e1fe3e4aSElliott Hughes        [lookup] = self.parse("lookup Foo useExtension {} Foo;").statements
727*e1fe3e4aSElliott Hughes        self.assertEqual(lookup.name, "Foo")
728*e1fe3e4aSElliott Hughes        self.assertTrue(lookup.use_extension)
729*e1fe3e4aSElliott Hughes        self.assertEqual(lookup.asFea(), "lookup Foo useExtension {\n    \n} Foo;\n")
730*e1fe3e4aSElliott Hughes
731*e1fe3e4aSElliott Hughes    def test_lookup_block_name_mismatch(self):
732*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
733*e1fe3e4aSElliott Hughes            FeatureLibError, 'Expected "Foo"', self.parse, "lookup Foo {} Bar;"
734*e1fe3e4aSElliott Hughes        )
735*e1fe3e4aSElliott Hughes
736*e1fe3e4aSElliott Hughes    def test_lookup_block_with_horizontal_valueRecordDef(self):
737*e1fe3e4aSElliott Hughes        doc = self.parse(
738*e1fe3e4aSElliott Hughes            "feature liga {"
739*e1fe3e4aSElliott Hughes            "  lookup look {"
740*e1fe3e4aSElliott Hughes            "    valueRecordDef 123 foo;"
741*e1fe3e4aSElliott Hughes            "  } look;"
742*e1fe3e4aSElliott Hughes            "} liga;"
743*e1fe3e4aSElliott Hughes        )
744*e1fe3e4aSElliott Hughes        [liga] = doc.statements
745*e1fe3e4aSElliott Hughes        [look] = liga.statements
746*e1fe3e4aSElliott Hughes        [foo] = look.statements
747*e1fe3e4aSElliott Hughes        self.assertEqual(foo.value.xAdvance, 123)
748*e1fe3e4aSElliott Hughes        self.assertIsNone(foo.value.yAdvance)
749*e1fe3e4aSElliott Hughes
750*e1fe3e4aSElliott Hughes    def test_lookup_block_with_vertical_valueRecordDef(self):
751*e1fe3e4aSElliott Hughes        doc = self.parse(
752*e1fe3e4aSElliott Hughes            "feature vkrn {"
753*e1fe3e4aSElliott Hughes            "  lookup look {"
754*e1fe3e4aSElliott Hughes            "    valueRecordDef 123 foo;"
755*e1fe3e4aSElliott Hughes            "  } look;"
756*e1fe3e4aSElliott Hughes            "} vkrn;"
757*e1fe3e4aSElliott Hughes        )
758*e1fe3e4aSElliott Hughes        [vkrn] = doc.statements
759*e1fe3e4aSElliott Hughes        [look] = vkrn.statements
760*e1fe3e4aSElliott Hughes        [foo] = look.statements
761*e1fe3e4aSElliott Hughes        self.assertIsNone(foo.value.xAdvance)
762*e1fe3e4aSElliott Hughes        self.assertEqual(foo.value.yAdvance, 123)
763*e1fe3e4aSElliott Hughes
764*e1fe3e4aSElliott Hughes    def test_lookup_comment(self):
765*e1fe3e4aSElliott Hughes        [lookup] = self.parse("lookup L { # Comment\n } L;").statements
766*e1fe3e4aSElliott Hughes        [comment] = lookup.statements
767*e1fe3e4aSElliott Hughes        self.assertIsInstance(comment, ast.Comment)
768*e1fe3e4aSElliott Hughes        self.assertEqual(comment.text, "# Comment")
769*e1fe3e4aSElliott Hughes
770*e1fe3e4aSElliott Hughes    def test_lookup_reference(self):
771*e1fe3e4aSElliott Hughes        [foo, bar] = self.parse(
772*e1fe3e4aSElliott Hughes            "lookup Foo {} Foo;" "feature Bar {lookup Foo;} Bar;"
773*e1fe3e4aSElliott Hughes        ).statements
774*e1fe3e4aSElliott Hughes        [ref] = bar.statements
775*e1fe3e4aSElliott Hughes        self.assertEqual(type(ref), ast.LookupReferenceStatement)
776*e1fe3e4aSElliott Hughes        self.assertEqual(ref.lookup, foo)
777*e1fe3e4aSElliott Hughes
778*e1fe3e4aSElliott Hughes    def test_lookup_reference_to_lookup_inside_feature(self):
779*e1fe3e4aSElliott Hughes        [qux, bar] = self.parse(
780*e1fe3e4aSElliott Hughes            "feature Qux {lookup Foo {} Foo;} Qux;" "feature Bar {lookup Foo;} Bar;"
781*e1fe3e4aSElliott Hughes        ).statements
782*e1fe3e4aSElliott Hughes        [foo] = qux.statements
783*e1fe3e4aSElliott Hughes        [ref] = bar.statements
784*e1fe3e4aSElliott Hughes        self.assertIsInstance(ref, ast.LookupReferenceStatement)
785*e1fe3e4aSElliott Hughes        self.assertEqual(ref.lookup, foo)
786*e1fe3e4aSElliott Hughes
787*e1fe3e4aSElliott Hughes    def test_lookup_reference_unknown(self):
788*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
789*e1fe3e4aSElliott Hughes            FeatureLibError,
790*e1fe3e4aSElliott Hughes            'Unknown lookup "Huh"',
791*e1fe3e4aSElliott Hughes            self.parse,
792*e1fe3e4aSElliott Hughes            "feature liga {lookup Huh;} liga;",
793*e1fe3e4aSElliott Hughes        )
794*e1fe3e4aSElliott Hughes
795*e1fe3e4aSElliott Hughes    def parse_lookupflag_(self, s):
796*e1fe3e4aSElliott Hughes        return self.parse("lookup L {%s} L;" % s).statements[0].statements[-1]
797*e1fe3e4aSElliott Hughes
798*e1fe3e4aSElliott Hughes    def test_lookupflag_format_A(self):
799*e1fe3e4aSElliott Hughes        flag = self.parse_lookupflag_("lookupflag RightToLeft IgnoreMarks;")
800*e1fe3e4aSElliott Hughes        self.assertIsInstance(flag, ast.LookupFlagStatement)
801*e1fe3e4aSElliott Hughes        self.assertEqual(flag.value, 9)
802*e1fe3e4aSElliott Hughes        self.assertIsNone(flag.markAttachment)
803*e1fe3e4aSElliott Hughes        self.assertIsNone(flag.markFilteringSet)
804*e1fe3e4aSElliott Hughes        self.assertEqual(flag.asFea(), "lookupflag RightToLeft IgnoreMarks;")
805*e1fe3e4aSElliott Hughes
806*e1fe3e4aSElliott Hughes    def test_lookupflag_format_A_MarkAttachmentType(self):
807*e1fe3e4aSElliott Hughes        flag = self.parse_lookupflag_(
808*e1fe3e4aSElliott Hughes            "@TOP_MARKS = [acute grave macron];"
809*e1fe3e4aSElliott Hughes            "lookupflag RightToLeft MarkAttachmentType @TOP_MARKS;"
810*e1fe3e4aSElliott Hughes        )
811*e1fe3e4aSElliott Hughes        self.assertIsInstance(flag, ast.LookupFlagStatement)
812*e1fe3e4aSElliott Hughes        self.assertEqual(flag.value, 1)
813*e1fe3e4aSElliott Hughes        self.assertIsInstance(flag.markAttachment, ast.GlyphClassName)
814*e1fe3e4aSElliott Hughes        self.assertEqual(flag.markAttachment.glyphSet(), ("acute", "grave", "macron"))
815*e1fe3e4aSElliott Hughes        self.assertIsNone(flag.markFilteringSet)
816*e1fe3e4aSElliott Hughes        self.assertEqual(
817*e1fe3e4aSElliott Hughes            flag.asFea(), "lookupflag RightToLeft MarkAttachmentType @TOP_MARKS;"
818*e1fe3e4aSElliott Hughes        )
819*e1fe3e4aSElliott Hughes
820*e1fe3e4aSElliott Hughes    def test_lookupflag_format_A_MarkAttachmentType_glyphClass(self):
821*e1fe3e4aSElliott Hughes        flag = self.parse_lookupflag_(
822*e1fe3e4aSElliott Hughes            "lookupflag RightToLeft MarkAttachmentType [acute grave macron];"
823*e1fe3e4aSElliott Hughes        )
824*e1fe3e4aSElliott Hughes        self.assertIsInstance(flag, ast.LookupFlagStatement)
825*e1fe3e4aSElliott Hughes        self.assertEqual(flag.value, 1)
826*e1fe3e4aSElliott Hughes        self.assertIsInstance(flag.markAttachment, ast.GlyphClass)
827*e1fe3e4aSElliott Hughes        self.assertEqual(flag.markAttachment.glyphSet(), ("acute", "grave", "macron"))
828*e1fe3e4aSElliott Hughes        self.assertIsNone(flag.markFilteringSet)
829*e1fe3e4aSElliott Hughes        self.assertEqual(
830*e1fe3e4aSElliott Hughes            flag.asFea(),
831*e1fe3e4aSElliott Hughes            "lookupflag RightToLeft MarkAttachmentType [acute grave macron];",
832*e1fe3e4aSElliott Hughes        )
833*e1fe3e4aSElliott Hughes
834*e1fe3e4aSElliott Hughes    def test_lookupflag_format_A_UseMarkFilteringSet(self):
835*e1fe3e4aSElliott Hughes        flag = self.parse_lookupflag_(
836*e1fe3e4aSElliott Hughes            "@BOTTOM_MARKS = [cedilla ogonek];"
837*e1fe3e4aSElliott Hughes            "lookupflag UseMarkFilteringSet @BOTTOM_MARKS IgnoreLigatures;"
838*e1fe3e4aSElliott Hughes        )
839*e1fe3e4aSElliott Hughes        self.assertIsInstance(flag, ast.LookupFlagStatement)
840*e1fe3e4aSElliott Hughes        self.assertEqual(flag.value, 4)
841*e1fe3e4aSElliott Hughes        self.assertIsNone(flag.markAttachment)
842*e1fe3e4aSElliott Hughes        self.assertIsInstance(flag.markFilteringSet, ast.GlyphClassName)
843*e1fe3e4aSElliott Hughes        self.assertEqual(flag.markFilteringSet.glyphSet(), ("cedilla", "ogonek"))
844*e1fe3e4aSElliott Hughes        self.assertEqual(
845*e1fe3e4aSElliott Hughes            flag.asFea(),
846*e1fe3e4aSElliott Hughes            "lookupflag IgnoreLigatures UseMarkFilteringSet @BOTTOM_MARKS;",
847*e1fe3e4aSElliott Hughes        )
848*e1fe3e4aSElliott Hughes
849*e1fe3e4aSElliott Hughes    def test_lookupflag_format_A_UseMarkFilteringSet_glyphClass(self):
850*e1fe3e4aSElliott Hughes        flag = self.parse_lookupflag_(
851*e1fe3e4aSElliott Hughes            "lookupflag UseMarkFilteringSet [cedilla ogonek] IgnoreLigatures;"
852*e1fe3e4aSElliott Hughes        )
853*e1fe3e4aSElliott Hughes        self.assertIsInstance(flag, ast.LookupFlagStatement)
854*e1fe3e4aSElliott Hughes        self.assertEqual(flag.value, 4)
855*e1fe3e4aSElliott Hughes        self.assertIsNone(flag.markAttachment)
856*e1fe3e4aSElliott Hughes        self.assertIsInstance(flag.markFilteringSet, ast.GlyphClass)
857*e1fe3e4aSElliott Hughes        self.assertEqual(flag.markFilteringSet.glyphSet(), ("cedilla", "ogonek"))
858*e1fe3e4aSElliott Hughes        self.assertEqual(
859*e1fe3e4aSElliott Hughes            flag.asFea(),
860*e1fe3e4aSElliott Hughes            "lookupflag IgnoreLigatures UseMarkFilteringSet [cedilla ogonek];",
861*e1fe3e4aSElliott Hughes        )
862*e1fe3e4aSElliott Hughes
863*e1fe3e4aSElliott Hughes    def test_lookupflag_format_B(self):
864*e1fe3e4aSElliott Hughes        flag = self.parse_lookupflag_("lookupflag 7;")
865*e1fe3e4aSElliott Hughes        self.assertIsInstance(flag, ast.LookupFlagStatement)
866*e1fe3e4aSElliott Hughes        self.assertEqual(flag.value, 7)
867*e1fe3e4aSElliott Hughes        self.assertIsNone(flag.markAttachment)
868*e1fe3e4aSElliott Hughes        self.assertIsNone(flag.markFilteringSet)
869*e1fe3e4aSElliott Hughes        self.assertEqual(
870*e1fe3e4aSElliott Hughes            flag.asFea(), "lookupflag RightToLeft IgnoreBaseGlyphs IgnoreLigatures;"
871*e1fe3e4aSElliott Hughes        )
872*e1fe3e4aSElliott Hughes
873*e1fe3e4aSElliott Hughes    def test_lookupflag_format_B_zero(self):
874*e1fe3e4aSElliott Hughes        flag = self.parse_lookupflag_("lookupflag 0;")
875*e1fe3e4aSElliott Hughes        self.assertIsInstance(flag, ast.LookupFlagStatement)
876*e1fe3e4aSElliott Hughes        self.assertEqual(flag.value, 0)
877*e1fe3e4aSElliott Hughes        self.assertIsNone(flag.markAttachment)
878*e1fe3e4aSElliott Hughes        self.assertIsNone(flag.markFilteringSet)
879*e1fe3e4aSElliott Hughes        self.assertEqual(flag.asFea(), "lookupflag 0;")
880*e1fe3e4aSElliott Hughes
881*e1fe3e4aSElliott Hughes    def test_lookupflag_no_value(self):
882*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
883*e1fe3e4aSElliott Hughes            FeatureLibError,
884*e1fe3e4aSElliott Hughes            "lookupflag must have a value",
885*e1fe3e4aSElliott Hughes            self.parse,
886*e1fe3e4aSElliott Hughes            "feature test {lookupflag;} test;",
887*e1fe3e4aSElliott Hughes        )
888*e1fe3e4aSElliott Hughes
889*e1fe3e4aSElliott Hughes    def test_lookupflag_repeated(self):
890*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
891*e1fe3e4aSElliott Hughes            FeatureLibError,
892*e1fe3e4aSElliott Hughes            "RightToLeft can be specified only once",
893*e1fe3e4aSElliott Hughes            self.parse,
894*e1fe3e4aSElliott Hughes            "feature test {lookupflag RightToLeft RightToLeft;} test;",
895*e1fe3e4aSElliott Hughes        )
896*e1fe3e4aSElliott Hughes
897*e1fe3e4aSElliott Hughes    def test_lookupflag_unrecognized(self):
898*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
899*e1fe3e4aSElliott Hughes            FeatureLibError,
900*e1fe3e4aSElliott Hughes            '"IgnoreCookies" is not a recognized lookupflag',
901*e1fe3e4aSElliott Hughes            self.parse,
902*e1fe3e4aSElliott Hughes            "feature test {lookupflag IgnoreCookies;} test;",
903*e1fe3e4aSElliott Hughes        )
904*e1fe3e4aSElliott Hughes
905*e1fe3e4aSElliott Hughes    def test_gpos_type_1_glyph(self):
906*e1fe3e4aSElliott Hughes        doc = self.parse("feature kern {pos one <1 2 3 4>;} kern;")
907*e1fe3e4aSElliott Hughes        pos = doc.statements[0].statements[0]
908*e1fe3e4aSElliott Hughes        self.assertIsInstance(pos, ast.SinglePosStatement)
909*e1fe3e4aSElliott Hughes        [(glyphs, value)] = pos.pos
910*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([glyphs]), "one")
911*e1fe3e4aSElliott Hughes        self.assertEqual(value.asFea(), "<1 2 3 4>")
912*e1fe3e4aSElliott Hughes
913*e1fe3e4aSElliott Hughes    def test_gpos_type_1_glyphclass_horizontal(self):
914*e1fe3e4aSElliott Hughes        doc = self.parse("feature kern {pos [one two] -300;} kern;")
915*e1fe3e4aSElliott Hughes        pos = doc.statements[0].statements[0]
916*e1fe3e4aSElliott Hughes        self.assertIsInstance(pos, ast.SinglePosStatement)
917*e1fe3e4aSElliott Hughes        [(glyphs, value)] = pos.pos
918*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([glyphs]), "[one two]")
919*e1fe3e4aSElliott Hughes        self.assertEqual(value.asFea(), "-300")
920*e1fe3e4aSElliott Hughes
921*e1fe3e4aSElliott Hughes    def test_gpos_type_1_glyphclass_vertical(self):
922*e1fe3e4aSElliott Hughes        doc = self.parse("feature vkrn {pos [one two] -300;} vkrn;")
923*e1fe3e4aSElliott Hughes        pos = doc.statements[0].statements[0]
924*e1fe3e4aSElliott Hughes        self.assertIsInstance(pos, ast.SinglePosStatement)
925*e1fe3e4aSElliott Hughes        [(glyphs, value)] = pos.pos
926*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([glyphs]), "[one two]")
927*e1fe3e4aSElliott Hughes        self.assertEqual(value.asFea(), "-300")
928*e1fe3e4aSElliott Hughes
929*e1fe3e4aSElliott Hughes    def test_gpos_type_1_multiple(self):
930*e1fe3e4aSElliott Hughes        doc = self.parse("feature f {pos one'1 two'2 [five six]'56;} f;")
931*e1fe3e4aSElliott Hughes        pos = doc.statements[0].statements[0]
932*e1fe3e4aSElliott Hughes        self.assertIsInstance(pos, ast.SinglePosStatement)
933*e1fe3e4aSElliott Hughes        [(glyphs1, val1), (glyphs2, val2), (glyphs3, val3)] = pos.pos
934*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([glyphs1]), "one")
935*e1fe3e4aSElliott Hughes        self.assertEqual(val1.asFea(), "1")
936*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([glyphs2]), "two")
937*e1fe3e4aSElliott Hughes        self.assertEqual(val2.asFea(), "2")
938*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([glyphs3]), "[five six]")
939*e1fe3e4aSElliott Hughes        self.assertEqual(val3.asFea(), "56")
940*e1fe3e4aSElliott Hughes        self.assertEqual(pos.prefix, [])
941*e1fe3e4aSElliott Hughes        self.assertEqual(pos.suffix, [])
942*e1fe3e4aSElliott Hughes
943*e1fe3e4aSElliott Hughes    def test_gpos_type_1_enumerated(self):
944*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
945*e1fe3e4aSElliott Hughes            FeatureLibError,
946*e1fe3e4aSElliott Hughes            '"enumerate" is only allowed with pair positionings',
947*e1fe3e4aSElliott Hughes            self.parse,
948*e1fe3e4aSElliott Hughes            "feature test {enum pos T 100;} test;",
949*e1fe3e4aSElliott Hughes        )
950*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
951*e1fe3e4aSElliott Hughes            FeatureLibError,
952*e1fe3e4aSElliott Hughes            '"enumerate" is only allowed with pair positionings',
953*e1fe3e4aSElliott Hughes            self.parse,
954*e1fe3e4aSElliott Hughes            "feature test {enumerate pos T 100;} test;",
955*e1fe3e4aSElliott Hughes        )
956*e1fe3e4aSElliott Hughes
957*e1fe3e4aSElliott Hughes    def test_gpos_type_1_chained(self):
958*e1fe3e4aSElliott Hughes        doc = self.parse("feature kern {pos [A B] [T Y]' 20 comma;} kern;")
959*e1fe3e4aSElliott Hughes        pos = doc.statements[0].statements[0]
960*e1fe3e4aSElliott Hughes        self.assertIsInstance(pos, ast.SinglePosStatement)
961*e1fe3e4aSElliott Hughes        [(glyphs, value)] = pos.pos
962*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([glyphs]), "[T Y]")
963*e1fe3e4aSElliott Hughes        self.assertEqual(value.asFea(), "20")
964*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(pos.prefix), "[A B]")
965*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(pos.suffix), "comma")
966*e1fe3e4aSElliott Hughes
967*e1fe3e4aSElliott Hughes    def test_gpos_type_1_chained_special_kern_format_valuerecord_format_a(self):
968*e1fe3e4aSElliott Hughes        doc = self.parse("feature kern {pos [A B] [T Y]' comma 20;} kern;")
969*e1fe3e4aSElliott Hughes        pos = doc.statements[0].statements[0]
970*e1fe3e4aSElliott Hughes        self.assertIsInstance(pos, ast.SinglePosStatement)
971*e1fe3e4aSElliott Hughes        [(glyphs, value)] = pos.pos
972*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([glyphs]), "[T Y]")
973*e1fe3e4aSElliott Hughes        self.assertEqual(value.asFea(), "20")
974*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(pos.prefix), "[A B]")
975*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(pos.suffix), "comma")
976*e1fe3e4aSElliott Hughes
977*e1fe3e4aSElliott Hughes    def test_gpos_type_1_chained_special_kern_format_valuerecord_format_b(self):
978*e1fe3e4aSElliott Hughes        doc = self.parse("feature kern {pos [A B] [T Y]' comma <0 0 0 0>;} kern;")
979*e1fe3e4aSElliott Hughes        pos = doc.statements[0].statements[0]
980*e1fe3e4aSElliott Hughes        self.assertIsInstance(pos, ast.SinglePosStatement)
981*e1fe3e4aSElliott Hughes        [(glyphs, value)] = pos.pos
982*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([glyphs]), "[T Y]")
983*e1fe3e4aSElliott Hughes        self.assertEqual(value.asFea(), "<0 0 0 0>")
984*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(pos.prefix), "[A B]")
985*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(pos.suffix), "comma")
986*e1fe3e4aSElliott Hughes
987*e1fe3e4aSElliott Hughes    def test_gpos_type_1_chained_special_kern_format_valuerecord_format_b_bug2293(self):
988*e1fe3e4aSElliott Hughes        # https://github.com/fonttools/fonttools/issues/2293
989*e1fe3e4aSElliott Hughes        doc = self.parse("feature kern {pos [A B] [T Y]' comma a <0 0 0 0>;} kern;")
990*e1fe3e4aSElliott Hughes        pos = doc.statements[0].statements[0]
991*e1fe3e4aSElliott Hughes        self.assertIsInstance(pos, ast.SinglePosStatement)
992*e1fe3e4aSElliott Hughes        [(glyphs, value)] = pos.pos
993*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([glyphs]), "[T Y]")
994*e1fe3e4aSElliott Hughes        self.assertEqual(value.asFea(), "<0 0 0 0>")
995*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(pos.prefix), "[A B]")
996*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(pos.suffix), "comma a")
997*e1fe3e4aSElliott Hughes
998*e1fe3e4aSElliott Hughes    def test_gpos_type_1_chained_exception1(self):
999*e1fe3e4aSElliott Hughes        with self.assertRaisesRegex(FeatureLibError, "Positioning values are allowed"):
1000*e1fe3e4aSElliott Hughes            doc = self.parse(
1001*e1fe3e4aSElliott Hughes                "feature kern {" "    pos [A B]' [T Y]' comma a <0 0 0 0>;" "} kern;"
1002*e1fe3e4aSElliott Hughes            )
1003*e1fe3e4aSElliott Hughes
1004*e1fe3e4aSElliott Hughes    def test_gpos_type_1_chained_exception2(self):
1005*e1fe3e4aSElliott Hughes        with self.assertRaisesRegex(FeatureLibError, "Positioning values are allowed"):
1006*e1fe3e4aSElliott Hughes            doc = self.parse(
1007*e1fe3e4aSElliott Hughes                "feature kern {"
1008*e1fe3e4aSElliott Hughes                "    pos [A B]' <0 0 0 0> [T Y]' comma a <0 0 0 0>;"
1009*e1fe3e4aSElliott Hughes                "} kern;"
1010*e1fe3e4aSElliott Hughes            )
1011*e1fe3e4aSElliott Hughes
1012*e1fe3e4aSElliott Hughes    def test_gpos_type_1_chained_exception3(self):
1013*e1fe3e4aSElliott Hughes        with self.assertRaisesRegex(FeatureLibError, "Positioning cannot be applied"):
1014*e1fe3e4aSElliott Hughes            doc = self.parse(
1015*e1fe3e4aSElliott Hughes                "feature kern {"
1016*e1fe3e4aSElliott Hughes                "    pos [A B] <0 0 0 0> [T Y]' comma a <0 0 0 0>;"
1017*e1fe3e4aSElliott Hughes                "} kern;"
1018*e1fe3e4aSElliott Hughes            )
1019*e1fe3e4aSElliott Hughes
1020*e1fe3e4aSElliott Hughes    def test_gpos_type_1_chained_exception4(self):
1021*e1fe3e4aSElliott Hughes        with self.assertRaisesRegex(FeatureLibError, "Positioning values are allowed"):
1022*e1fe3e4aSElliott Hughes            doc = self.parse("feature kern {" "    pos a' b c 123 d;" "} kern;")
1023*e1fe3e4aSElliott Hughes
1024*e1fe3e4aSElliott Hughes    def test_gpos_type_2_format_a(self):
1025*e1fe3e4aSElliott Hughes        doc = self.parse(
1026*e1fe3e4aSElliott Hughes            "feature kern {" "    pos [T V] -60 [a b c] <1 2 3 4>;" "} kern;"
1027*e1fe3e4aSElliott Hughes        )
1028*e1fe3e4aSElliott Hughes        pos = doc.statements[0].statements[0]
1029*e1fe3e4aSElliott Hughes        self.assertEqual(type(pos), ast.PairPosStatement)
1030*e1fe3e4aSElliott Hughes        self.assertFalse(pos.enumerated)
1031*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([pos.glyphs1]), "[T V]")
1032*e1fe3e4aSElliott Hughes        self.assertEqual(pos.valuerecord1.asFea(), "-60")
1033*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([pos.glyphs2]), "[a b c]")
1034*e1fe3e4aSElliott Hughes        self.assertEqual(pos.valuerecord2.asFea(), "<1 2 3 4>")
1035*e1fe3e4aSElliott Hughes
1036*e1fe3e4aSElliott Hughes    def test_gpos_type_2_format_a_enumerated(self):
1037*e1fe3e4aSElliott Hughes        doc = self.parse(
1038*e1fe3e4aSElliott Hughes            "feature kern {" "    enum pos [T V] -60 [a b c] <1 2 3 4>;" "} kern;"
1039*e1fe3e4aSElliott Hughes        )
1040*e1fe3e4aSElliott Hughes        pos = doc.statements[0].statements[0]
1041*e1fe3e4aSElliott Hughes        self.assertEqual(type(pos), ast.PairPosStatement)
1042*e1fe3e4aSElliott Hughes        self.assertTrue(pos.enumerated)
1043*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([pos.glyphs1]), "[T V]")
1044*e1fe3e4aSElliott Hughes        self.assertEqual(pos.valuerecord1.asFea(), "-60")
1045*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([pos.glyphs2]), "[a b c]")
1046*e1fe3e4aSElliott Hughes        self.assertEqual(pos.valuerecord2.asFea(), "<1 2 3 4>")
1047*e1fe3e4aSElliott Hughes
1048*e1fe3e4aSElliott Hughes    def test_gpos_type_2_format_a_with_null_first(self):
1049*e1fe3e4aSElliott Hughes        doc = self.parse(
1050*e1fe3e4aSElliott Hughes            "feature kern {" "    pos [T V] <NULL> [a b c] <1 2 3 4>;" "} kern;"
1051*e1fe3e4aSElliott Hughes        )
1052*e1fe3e4aSElliott Hughes        pos = doc.statements[0].statements[0]
1053*e1fe3e4aSElliott Hughes        self.assertEqual(type(pos), ast.PairPosStatement)
1054*e1fe3e4aSElliott Hughes        self.assertFalse(pos.enumerated)
1055*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([pos.glyphs1]), "[T V]")
1056*e1fe3e4aSElliott Hughes        self.assertFalse(pos.valuerecord1)
1057*e1fe3e4aSElliott Hughes        self.assertEqual(pos.valuerecord1.asFea(), "<NULL>")
1058*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([pos.glyphs2]), "[a b c]")
1059*e1fe3e4aSElliott Hughes        self.assertEqual(pos.valuerecord2.asFea(), "<1 2 3 4>")
1060*e1fe3e4aSElliott Hughes        self.assertEqual(pos.asFea(), "pos [T V] <NULL> [a b c] <1 2 3 4>;")
1061*e1fe3e4aSElliott Hughes
1062*e1fe3e4aSElliott Hughes    def test_gpos_type_2_format_a_with_null_second(self):
1063*e1fe3e4aSElliott Hughes        doc = self.parse(
1064*e1fe3e4aSElliott Hughes            "feature kern {" "    pos [T V] <1 2 3 4> [a b c] <NULL>;" "} kern;"
1065*e1fe3e4aSElliott Hughes        )
1066*e1fe3e4aSElliott Hughes        pos = doc.statements[0].statements[0]
1067*e1fe3e4aSElliott Hughes        self.assertEqual(type(pos), ast.PairPosStatement)
1068*e1fe3e4aSElliott Hughes        self.assertFalse(pos.enumerated)
1069*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([pos.glyphs1]), "[T V]")
1070*e1fe3e4aSElliott Hughes        self.assertEqual(pos.valuerecord1.asFea(), "<1 2 3 4>")
1071*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([pos.glyphs2]), "[a b c]")
1072*e1fe3e4aSElliott Hughes        self.assertFalse(pos.valuerecord2)
1073*e1fe3e4aSElliott Hughes        self.assertEqual(pos.asFea(), "pos [T V] [a b c] <1 2 3 4>;")
1074*e1fe3e4aSElliott Hughes
1075*e1fe3e4aSElliott Hughes    def test_gpos_type_2_format_b(self):
1076*e1fe3e4aSElliott Hughes        doc = self.parse("feature kern {" "    pos [T V] [a b c] <1 2 3 4>;" "} kern;")
1077*e1fe3e4aSElliott Hughes        pos = doc.statements[0].statements[0]
1078*e1fe3e4aSElliott Hughes        self.assertEqual(type(pos), ast.PairPosStatement)
1079*e1fe3e4aSElliott Hughes        self.assertFalse(pos.enumerated)
1080*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([pos.glyphs1]), "[T V]")
1081*e1fe3e4aSElliott Hughes        self.assertEqual(pos.valuerecord1.asFea(), "<1 2 3 4>")
1082*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([pos.glyphs2]), "[a b c]")
1083*e1fe3e4aSElliott Hughes        self.assertIsNone(pos.valuerecord2)
1084*e1fe3e4aSElliott Hughes
1085*e1fe3e4aSElliott Hughes    def test_gpos_type_2_format_b_enumerated(self):
1086*e1fe3e4aSElliott Hughes        doc = self.parse(
1087*e1fe3e4aSElliott Hughes            "feature kern {" "    enumerate position [T V] [a b c] <1 2 3 4>;" "} kern;"
1088*e1fe3e4aSElliott Hughes        )
1089*e1fe3e4aSElliott Hughes        pos = doc.statements[0].statements[0]
1090*e1fe3e4aSElliott Hughes        self.assertEqual(type(pos), ast.PairPosStatement)
1091*e1fe3e4aSElliott Hughes        self.assertTrue(pos.enumerated)
1092*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([pos.glyphs1]), "[T V]")
1093*e1fe3e4aSElliott Hughes        self.assertEqual(pos.valuerecord1.asFea(), "<1 2 3 4>")
1094*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([pos.glyphs2]), "[a b c]")
1095*e1fe3e4aSElliott Hughes        self.assertIsNone(pos.valuerecord2)
1096*e1fe3e4aSElliott Hughes
1097*e1fe3e4aSElliott Hughes    def test_gpos_type_3(self):
1098*e1fe3e4aSElliott Hughes        doc = self.parse(
1099*e1fe3e4aSElliott Hughes            "feature kern {"
1100*e1fe3e4aSElliott Hughes            "    position cursive A <anchor 12 -2> <anchor 2 3>;"
1101*e1fe3e4aSElliott Hughes            "} kern;"
1102*e1fe3e4aSElliott Hughes        )
1103*e1fe3e4aSElliott Hughes        pos = doc.statements[0].statements[0]
1104*e1fe3e4aSElliott Hughes        self.assertEqual(type(pos), ast.CursivePosStatement)
1105*e1fe3e4aSElliott Hughes        self.assertEqual(pos.glyphclass.glyphSet(), ("A",))
1106*e1fe3e4aSElliott Hughes        self.assertEqual((pos.entryAnchor.x, pos.entryAnchor.y), (12, -2))
1107*e1fe3e4aSElliott Hughes        self.assertEqual((pos.exitAnchor.x, pos.exitAnchor.y), (2, 3))
1108*e1fe3e4aSElliott Hughes
1109*e1fe3e4aSElliott Hughes    def test_gpos_type_3_enumerated(self):
1110*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
1111*e1fe3e4aSElliott Hughes            FeatureLibError,
1112*e1fe3e4aSElliott Hughes            '"enumerate" is not allowed with cursive attachment positioning',
1113*e1fe3e4aSElliott Hughes            self.parse,
1114*e1fe3e4aSElliott Hughes            "feature kern {"
1115*e1fe3e4aSElliott Hughes            "    enumerate position cursive A <anchor 12 -2> <anchor 2 3>;"
1116*e1fe3e4aSElliott Hughes            "} kern;",
1117*e1fe3e4aSElliott Hughes        )
1118*e1fe3e4aSElliott Hughes
1119*e1fe3e4aSElliott Hughes    def test_gpos_type_4(self):
1120*e1fe3e4aSElliott Hughes        doc = self.parse(
1121*e1fe3e4aSElliott Hughes            "markClass [acute grave] <anchor 150 -10> @TOP_MARKS;"
1122*e1fe3e4aSElliott Hughes            "markClass [dieresis umlaut] <anchor 300 -10> @TOP_MARKS;"
1123*e1fe3e4aSElliott Hughes            "markClass [cedilla] <anchor 300 600> @BOTTOM_MARKS;"
1124*e1fe3e4aSElliott Hughes            "feature test {"
1125*e1fe3e4aSElliott Hughes            "    position base [a e o u] "
1126*e1fe3e4aSElliott Hughes            "        <anchor 250 450> mark @TOP_MARKS "
1127*e1fe3e4aSElliott Hughes            "        <anchor 210 -10> mark @BOTTOM_MARKS;"
1128*e1fe3e4aSElliott Hughes            "} test;"
1129*e1fe3e4aSElliott Hughes        )
1130*e1fe3e4aSElliott Hughes        pos = doc.statements[-1].statements[0]
1131*e1fe3e4aSElliott Hughes        self.assertEqual(type(pos), ast.MarkBasePosStatement)
1132*e1fe3e4aSElliott Hughes        self.assertEqual(pos.base.glyphSet(), ("a", "e", "o", "u"))
1133*e1fe3e4aSElliott Hughes        (a1, m1), (a2, m2) = pos.marks
1134*e1fe3e4aSElliott Hughes        self.assertEqual((a1.x, a1.y, m1.name), (250, 450, "TOP_MARKS"))
1135*e1fe3e4aSElliott Hughes        self.assertEqual((a2.x, a2.y, m2.name), (210, -10, "BOTTOM_MARKS"))
1136*e1fe3e4aSElliott Hughes
1137*e1fe3e4aSElliott Hughes    def test_gpos_type_4_enumerated(self):
1138*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
1139*e1fe3e4aSElliott Hughes            FeatureLibError,
1140*e1fe3e4aSElliott Hughes            '"enumerate" is not allowed with ' "mark-to-base attachment positioning",
1141*e1fe3e4aSElliott Hughes            self.parse,
1142*e1fe3e4aSElliott Hughes            "feature kern {"
1143*e1fe3e4aSElliott Hughes            "    markClass cedilla <anchor 300 600> @BOTTOM_MARKS;"
1144*e1fe3e4aSElliott Hughes            "    enumerate position base A <anchor 12 -2> mark @BOTTOM_MARKS;"
1145*e1fe3e4aSElliott Hughes            "} kern;",
1146*e1fe3e4aSElliott Hughes        )
1147*e1fe3e4aSElliott Hughes
1148*e1fe3e4aSElliott Hughes    def test_gpos_type_4_not_markClass(self):
1149*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
1150*e1fe3e4aSElliott Hughes            FeatureLibError,
1151*e1fe3e4aSElliott Hughes            "@MARKS is not a markClass",
1152*e1fe3e4aSElliott Hughes            self.parse,
1153*e1fe3e4aSElliott Hughes            "@MARKS = [acute grave];"
1154*e1fe3e4aSElliott Hughes            "feature test {"
1155*e1fe3e4aSElliott Hughes            "    position base [a e o u] <anchor 250 450> mark @MARKS;"
1156*e1fe3e4aSElliott Hughes            "} test;",
1157*e1fe3e4aSElliott Hughes        )
1158*e1fe3e4aSElliott Hughes
1159*e1fe3e4aSElliott Hughes    def test_gpos_type_5(self):
1160*e1fe3e4aSElliott Hughes        doc = self.parse(
1161*e1fe3e4aSElliott Hughes            "markClass [grave acute] <anchor 150 500> @TOP_MARKS;"
1162*e1fe3e4aSElliott Hughes            "markClass [cedilla] <anchor 300 -100> @BOTTOM_MARKS;"
1163*e1fe3e4aSElliott Hughes            "feature test {"
1164*e1fe3e4aSElliott Hughes            "    position "
1165*e1fe3e4aSElliott Hughes            "        ligature [a_f_f_i o_f_f_i] "
1166*e1fe3e4aSElliott Hughes            "            <anchor 50 600> mark @TOP_MARKS "
1167*e1fe3e4aSElliott Hughes            "            <anchor 50 -10> mark @BOTTOM_MARKS "
1168*e1fe3e4aSElliott Hughes            "        ligComponent "
1169*e1fe3e4aSElliott Hughes            "            <anchor 30 800> mark @TOP_MARKS "
1170*e1fe3e4aSElliott Hughes            "        ligComponent "
1171*e1fe3e4aSElliott Hughes            "            <anchor NULL> "
1172*e1fe3e4aSElliott Hughes            "        ligComponent "
1173*e1fe3e4aSElliott Hughes            "            <anchor 30 -10> mark @BOTTOM_MARKS;"
1174*e1fe3e4aSElliott Hughes            "} test;"
1175*e1fe3e4aSElliott Hughes        )
1176*e1fe3e4aSElliott Hughes        pos = doc.statements[-1].statements[0]
1177*e1fe3e4aSElliott Hughes        self.assertEqual(type(pos), ast.MarkLigPosStatement)
1178*e1fe3e4aSElliott Hughes        self.assertEqual(pos.ligatures.glyphSet(), ("a_f_f_i", "o_f_f_i"))
1179*e1fe3e4aSElliott Hughes        [(a11, m11), (a12, m12)], [(a2, m2)], [], [(a4, m4)] = pos.marks
1180*e1fe3e4aSElliott Hughes        self.assertEqual((a11.x, a11.y, m11.name), (50, 600, "TOP_MARKS"))
1181*e1fe3e4aSElliott Hughes        self.assertEqual((a12.x, a12.y, m12.name), (50, -10, "BOTTOM_MARKS"))
1182*e1fe3e4aSElliott Hughes        self.assertEqual((a2.x, a2.y, m2.name), (30, 800, "TOP_MARKS"))
1183*e1fe3e4aSElliott Hughes        self.assertEqual((a4.x, a4.y, m4.name), (30, -10, "BOTTOM_MARKS"))
1184*e1fe3e4aSElliott Hughes
1185*e1fe3e4aSElliott Hughes    def test_gpos_type_5_enumerated(self):
1186*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
1187*e1fe3e4aSElliott Hughes            FeatureLibError,
1188*e1fe3e4aSElliott Hughes            '"enumerate" is not allowed with '
1189*e1fe3e4aSElliott Hughes            "mark-to-ligature attachment positioning",
1190*e1fe3e4aSElliott Hughes            self.parse,
1191*e1fe3e4aSElliott Hughes            "feature test {"
1192*e1fe3e4aSElliott Hughes            "    markClass cedilla <anchor 300 600> @MARKS;"
1193*e1fe3e4aSElliott Hughes            "    enumerate position "
1194*e1fe3e4aSElliott Hughes            "        ligature f_i <anchor 100 0> mark @MARKS"
1195*e1fe3e4aSElliott Hughes            "        ligComponent <anchor NULL>;"
1196*e1fe3e4aSElliott Hughes            "} test;",
1197*e1fe3e4aSElliott Hughes        )
1198*e1fe3e4aSElliott Hughes
1199*e1fe3e4aSElliott Hughes    def test_gpos_type_5_not_markClass(self):
1200*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
1201*e1fe3e4aSElliott Hughes            FeatureLibError,
1202*e1fe3e4aSElliott Hughes            "@MARKS is not a markClass",
1203*e1fe3e4aSElliott Hughes            self.parse,
1204*e1fe3e4aSElliott Hughes            "@MARKS = [acute grave];"
1205*e1fe3e4aSElliott Hughes            "feature test {"
1206*e1fe3e4aSElliott Hughes            "    position ligature f_i <anchor 250 450> mark @MARKS;"
1207*e1fe3e4aSElliott Hughes            "} test;",
1208*e1fe3e4aSElliott Hughes        )
1209*e1fe3e4aSElliott Hughes
1210*e1fe3e4aSElliott Hughes    def test_gpos_type_6(self):
1211*e1fe3e4aSElliott Hughes        doc = self.parse(
1212*e1fe3e4aSElliott Hughes            "markClass damma <anchor 189 -103> @MARK_CLASS_1;"
1213*e1fe3e4aSElliott Hughes            "feature test {"
1214*e1fe3e4aSElliott Hughes            "    position mark hamza <anchor 221 301> mark @MARK_CLASS_1;"
1215*e1fe3e4aSElliott Hughes            "} test;"
1216*e1fe3e4aSElliott Hughes        )
1217*e1fe3e4aSElliott Hughes        pos = doc.statements[-1].statements[0]
1218*e1fe3e4aSElliott Hughes        self.assertEqual(type(pos), ast.MarkMarkPosStatement)
1219*e1fe3e4aSElliott Hughes        self.assertEqual(pos.baseMarks.glyphSet(), ("hamza",))
1220*e1fe3e4aSElliott Hughes        [(a1, m1)] = pos.marks
1221*e1fe3e4aSElliott Hughes        self.assertEqual((a1.x, a1.y, m1.name), (221, 301, "MARK_CLASS_1"))
1222*e1fe3e4aSElliott Hughes
1223*e1fe3e4aSElliott Hughes    def test_gpos_type_6_enumerated(self):
1224*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
1225*e1fe3e4aSElliott Hughes            FeatureLibError,
1226*e1fe3e4aSElliott Hughes            '"enumerate" is not allowed with ' "mark-to-mark attachment positioning",
1227*e1fe3e4aSElliott Hughes            self.parse,
1228*e1fe3e4aSElliott Hughes            "markClass damma <anchor 189 -103> @MARK_CLASS_1;"
1229*e1fe3e4aSElliott Hughes            "feature test {"
1230*e1fe3e4aSElliott Hughes            "    enum pos mark hamza <anchor 221 301> mark @MARK_CLASS_1;"
1231*e1fe3e4aSElliott Hughes            "} test;",
1232*e1fe3e4aSElliott Hughes        )
1233*e1fe3e4aSElliott Hughes
1234*e1fe3e4aSElliott Hughes    def test_gpos_type_6_not_markClass(self):
1235*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
1236*e1fe3e4aSElliott Hughes            FeatureLibError,
1237*e1fe3e4aSElliott Hughes            "@MARKS is not a markClass",
1238*e1fe3e4aSElliott Hughes            self.parse,
1239*e1fe3e4aSElliott Hughes            "@MARKS = [acute grave];"
1240*e1fe3e4aSElliott Hughes            "feature test {"
1241*e1fe3e4aSElliott Hughes            "    position mark cedilla <anchor 250 450> mark @MARKS;"
1242*e1fe3e4aSElliott Hughes            "} test;",
1243*e1fe3e4aSElliott Hughes        )
1244*e1fe3e4aSElliott Hughes
1245*e1fe3e4aSElliott Hughes    def test_gpos_type_8(self):
1246*e1fe3e4aSElliott Hughes        doc = self.parse(
1247*e1fe3e4aSElliott Hughes            "lookup L1 {pos one 100;} L1; lookup L2 {pos two 200;} L2;"
1248*e1fe3e4aSElliott Hughes            "feature test {"
1249*e1fe3e4aSElliott Hughes            "    pos [A a] [B b] I' lookup L1 [N n]' lookup L2 P' [Y y] [Z z];"
1250*e1fe3e4aSElliott Hughes            "} test;"
1251*e1fe3e4aSElliott Hughes        )
1252*e1fe3e4aSElliott Hughes        lookup1, lookup2 = doc.statements[0:2]
1253*e1fe3e4aSElliott Hughes        pos = doc.statements[-1].statements[0]
1254*e1fe3e4aSElliott Hughes        self.assertEqual(type(pos), ast.ChainContextPosStatement)
1255*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(pos.prefix), "[A a] [B b]")
1256*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(pos.glyphs), "I [N n] P")
1257*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(pos.suffix), "[Y y] [Z z]")
1258*e1fe3e4aSElliott Hughes        self.assertEqual(pos.lookups, [[lookup1], [lookup2], None])
1259*e1fe3e4aSElliott Hughes
1260*e1fe3e4aSElliott Hughes    def test_gpos_type_8_lookup_with_values(self):
1261*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
1262*e1fe3e4aSElliott Hughes            FeatureLibError,
1263*e1fe3e4aSElliott Hughes            'If "lookup" is present, no values must be specified',
1264*e1fe3e4aSElliott Hughes            self.parse,
1265*e1fe3e4aSElliott Hughes            "lookup L1 {pos one 100;} L1;"
1266*e1fe3e4aSElliott Hughes            "feature test {"
1267*e1fe3e4aSElliott Hughes            "    pos A' lookup L1 B' 20;"
1268*e1fe3e4aSElliott Hughes            "} test;",
1269*e1fe3e4aSElliott Hughes        )
1270*e1fe3e4aSElliott Hughes
1271*e1fe3e4aSElliott Hughes    def test_markClass(self):
1272*e1fe3e4aSElliott Hughes        doc = self.parse("markClass [acute grave] <anchor 350 3> @MARKS;")
1273*e1fe3e4aSElliott Hughes        mc = doc.statements[0]
1274*e1fe3e4aSElliott Hughes        self.assertIsInstance(mc, ast.MarkClassDefinition)
1275*e1fe3e4aSElliott Hughes        self.assertEqual(mc.markClass.name, "MARKS")
1276*e1fe3e4aSElliott Hughes        self.assertEqual(mc.glyphSet(), ("acute", "grave"))
1277*e1fe3e4aSElliott Hughes        self.assertEqual((mc.anchor.x, mc.anchor.y), (350, 3))
1278*e1fe3e4aSElliott Hughes
1279*e1fe3e4aSElliott Hughes    def test_nameid_windows_utf16(self):
1280*e1fe3e4aSElliott Hughes        doc = self.parse(r'table name { nameid 9 "M\00fcller-Lanc\00e9"; } name;')
1281*e1fe3e4aSElliott Hughes        name = doc.statements[0].statements[0]
1282*e1fe3e4aSElliott Hughes        self.assertIsInstance(name, ast.NameRecord)
1283*e1fe3e4aSElliott Hughes        self.assertEqual(name.nameID, 9)
1284*e1fe3e4aSElliott Hughes        self.assertEqual(name.platformID, 3)
1285*e1fe3e4aSElliott Hughes        self.assertEqual(name.platEncID, 1)
1286*e1fe3e4aSElliott Hughes        self.assertEqual(name.langID, 0x0409)
1287*e1fe3e4aSElliott Hughes        self.assertEqual(name.string, "Müller-Lancé")
1288*e1fe3e4aSElliott Hughes        self.assertEqual(name.asFea(), r'nameid 9 "M\00fcller-Lanc\00e9";')
1289*e1fe3e4aSElliott Hughes
1290*e1fe3e4aSElliott Hughes    def test_nameid_windows_utf16_backslash(self):
1291*e1fe3e4aSElliott Hughes        doc = self.parse(r'table name { nameid 9 "Back\005cslash"; } name;')
1292*e1fe3e4aSElliott Hughes        name = doc.statements[0].statements[0]
1293*e1fe3e4aSElliott Hughes        self.assertEqual(name.string, r"Back\slash")
1294*e1fe3e4aSElliott Hughes        self.assertEqual(name.asFea(), r'nameid 9 "Back\005cslash";')
1295*e1fe3e4aSElliott Hughes
1296*e1fe3e4aSElliott Hughes    def test_nameid_windows_utf16_quotation_mark(self):
1297*e1fe3e4aSElliott Hughes        doc = self.parse(r'table name { nameid 9 "Quotation \0022Mark\0022"; } name;')
1298*e1fe3e4aSElliott Hughes        name = doc.statements[0].statements[0]
1299*e1fe3e4aSElliott Hughes        self.assertEqual(name.string, 'Quotation "Mark"')
1300*e1fe3e4aSElliott Hughes        self.assertEqual(name.asFea(), r'nameid 9 "Quotation \0022Mark\0022";')
1301*e1fe3e4aSElliott Hughes
1302*e1fe3e4aSElliott Hughes    def test_nameid_windows_utf16_surroates(self):
1303*e1fe3e4aSElliott Hughes        doc = self.parse(r'table name { nameid 9 "Carrot \D83E\DD55"; } name;')
1304*e1fe3e4aSElliott Hughes        name = doc.statements[0].statements[0]
1305*e1fe3e4aSElliott Hughes        self.assertEqual(name.string, r"Carrot ��")
1306*e1fe3e4aSElliott Hughes        self.assertEqual(name.asFea(), r'nameid 9 "Carrot \d83e\dd55";')
1307*e1fe3e4aSElliott Hughes
1308*e1fe3e4aSElliott Hughes    def test_nameid_mac_roman(self):
1309*e1fe3e4aSElliott Hughes        doc = self.parse(r'table name { nameid 9 1 "Joachim M\9fller-Lanc\8e"; } name;')
1310*e1fe3e4aSElliott Hughes        name = doc.statements[0].statements[0]
1311*e1fe3e4aSElliott Hughes        self.assertIsInstance(name, ast.NameRecord)
1312*e1fe3e4aSElliott Hughes        self.assertEqual(name.nameID, 9)
1313*e1fe3e4aSElliott Hughes        self.assertEqual(name.platformID, 1)
1314*e1fe3e4aSElliott Hughes        self.assertEqual(name.platEncID, 0)
1315*e1fe3e4aSElliott Hughes        self.assertEqual(name.langID, 0)
1316*e1fe3e4aSElliott Hughes        self.assertEqual(name.string, "Joachim Müller-Lancé")
1317*e1fe3e4aSElliott Hughes        self.assertEqual(name.asFea(), r'nameid 9 1 "Joachim M\9fller-Lanc\8e";')
1318*e1fe3e4aSElliott Hughes
1319*e1fe3e4aSElliott Hughes    def test_nameid_mac_croatian(self):
1320*e1fe3e4aSElliott Hughes        doc = self.parse(r'table name { nameid 9 1 0 18 "Jovica Veljovi\e6"; } name;')
1321*e1fe3e4aSElliott Hughes        name = doc.statements[0].statements[0]
1322*e1fe3e4aSElliott Hughes        self.assertEqual(name.nameID, 9)
1323*e1fe3e4aSElliott Hughes        self.assertEqual(name.platformID, 1)
1324*e1fe3e4aSElliott Hughes        self.assertEqual(name.platEncID, 0)
1325*e1fe3e4aSElliott Hughes        self.assertEqual(name.langID, 18)
1326*e1fe3e4aSElliott Hughes        self.assertEqual(name.string, "Jovica Veljović")
1327*e1fe3e4aSElliott Hughes        self.assertEqual(name.asFea(), r'nameid 9 1 0 18 "Jovica Veljovi\e6";')
1328*e1fe3e4aSElliott Hughes
1329*e1fe3e4aSElliott Hughes    def test_nameid_unsupported_platform(self):
1330*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
1331*e1fe3e4aSElliott Hughes            FeatureLibError,
1332*e1fe3e4aSElliott Hughes            "Expected platform id 1 or 3",
1333*e1fe3e4aSElliott Hughes            self.parse,
1334*e1fe3e4aSElliott Hughes            'table name { nameid 9 666 "Foo"; } name;',
1335*e1fe3e4aSElliott Hughes        )
1336*e1fe3e4aSElliott Hughes
1337*e1fe3e4aSElliott Hughes    def test_nameid_hexadecimal(self):
1338*e1fe3e4aSElliott Hughes        doc = self.parse(r'table name { nameid 0x9 0x3 0x1 0x0409 "Test"; } name;')
1339*e1fe3e4aSElliott Hughes        name = doc.statements[0].statements[0]
1340*e1fe3e4aSElliott Hughes        self.assertEqual(name.nameID, 9)
1341*e1fe3e4aSElliott Hughes        self.assertEqual(name.platformID, 3)
1342*e1fe3e4aSElliott Hughes        self.assertEqual(name.platEncID, 1)
1343*e1fe3e4aSElliott Hughes        self.assertEqual(name.langID, 0x0409)
1344*e1fe3e4aSElliott Hughes
1345*e1fe3e4aSElliott Hughes    def test_nameid_octal(self):
1346*e1fe3e4aSElliott Hughes        doc = self.parse(r'table name { nameid 011 03 012 02011 "Test"; } name;')
1347*e1fe3e4aSElliott Hughes        name = doc.statements[0].statements[0]
1348*e1fe3e4aSElliott Hughes        self.assertEqual(name.nameID, 9)
1349*e1fe3e4aSElliott Hughes        self.assertEqual(name.platformID, 3)
1350*e1fe3e4aSElliott Hughes        self.assertEqual(name.platEncID, 10)
1351*e1fe3e4aSElliott Hughes        self.assertEqual(name.langID, 0o2011)
1352*e1fe3e4aSElliott Hughes
1353*e1fe3e4aSElliott Hughes    def test_cv_hexadecimal(self):
1354*e1fe3e4aSElliott Hughes        doc = self.parse(r"feature cv01 { cvParameters { Character 0x5DDE; }; } cv01;")
1355*e1fe3e4aSElliott Hughes        cv = doc.statements[0].statements[0].statements[0]
1356*e1fe3e4aSElliott Hughes        self.assertEqual(cv.character, 0x5DDE)
1357*e1fe3e4aSElliott Hughes
1358*e1fe3e4aSElliott Hughes    def test_cv_octal(self):
1359*e1fe3e4aSElliott Hughes        doc = self.parse(r"feature cv01 { cvParameters { Character 056736; }; } cv01;")
1360*e1fe3e4aSElliott Hughes        cv = doc.statements[0].statements[0].statements[0]
1361*e1fe3e4aSElliott Hughes        self.assertEqual(cv.character, 0o56736)
1362*e1fe3e4aSElliott Hughes
1363*e1fe3e4aSElliott Hughes    def test_rsub_format_a(self):
1364*e1fe3e4aSElliott Hughes        doc = self.parse("feature test {rsub a [b B] c' d [e E] by C;} test;")
1365*e1fe3e4aSElliott Hughes        rsub = doc.statements[0].statements[0]
1366*e1fe3e4aSElliott Hughes        self.assertEqual(type(rsub), ast.ReverseChainSingleSubstStatement)
1367*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(rsub.old_prefix), "a [B b]")
1368*e1fe3e4aSElliott Hughes        self.assertEqual(rsub.glyphs[0].glyphSet(), ("c",))
1369*e1fe3e4aSElliott Hughes        self.assertEqual(rsub.replacements[0].glyphSet(), ("C",))
1370*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(rsub.old_suffix), "d [E e]")
1371*e1fe3e4aSElliott Hughes
1372*e1fe3e4aSElliott Hughes    def test_rsub_format_a_cid(self):
1373*e1fe3e4aSElliott Hughes        doc = self.parse(r"feature test {rsub \1 [\2 \3] \4' \5 by \6;} test;")
1374*e1fe3e4aSElliott Hughes        rsub = doc.statements[0].statements[0]
1375*e1fe3e4aSElliott Hughes        self.assertEqual(type(rsub), ast.ReverseChainSingleSubstStatement)
1376*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(rsub.old_prefix), "cid00001 [cid00002 cid00003]")
1377*e1fe3e4aSElliott Hughes        self.assertEqual(rsub.glyphs[0].glyphSet(), ("cid00004",))
1378*e1fe3e4aSElliott Hughes        self.assertEqual(rsub.replacements[0].glyphSet(), ("cid00006",))
1379*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(rsub.old_suffix), "cid00005")
1380*e1fe3e4aSElliott Hughes
1381*e1fe3e4aSElliott Hughes    def test_rsub_format_b(self):
1382*e1fe3e4aSElliott Hughes        doc = self.parse(
1383*e1fe3e4aSElliott Hughes            "feature smcp {"
1384*e1fe3e4aSElliott Hughes            "    reversesub A B [one.fitted one.oldstyle]' C [d D] by one;"
1385*e1fe3e4aSElliott Hughes            "} smcp;"
1386*e1fe3e4aSElliott Hughes        )
1387*e1fe3e4aSElliott Hughes        rsub = doc.statements[0].statements[0]
1388*e1fe3e4aSElliott Hughes        self.assertEqual(type(rsub), ast.ReverseChainSingleSubstStatement)
1389*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(rsub.old_prefix), "A B")
1390*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(rsub.old_suffix), "C [D d]")
1391*e1fe3e4aSElliott Hughes        self.assertEqual(mapping(rsub), {"one.fitted": "one", "one.oldstyle": "one"})
1392*e1fe3e4aSElliott Hughes
1393*e1fe3e4aSElliott Hughes    def test_rsub_format_c(self):
1394*e1fe3e4aSElliott Hughes        doc = self.parse(
1395*e1fe3e4aSElliott Hughes            "feature test {"
1396*e1fe3e4aSElliott Hughes            "    reversesub BACK TRACK [a-d]' LOOK AHEAD by [A.sc-D.sc];"
1397*e1fe3e4aSElliott Hughes            "} test;"
1398*e1fe3e4aSElliott Hughes        )
1399*e1fe3e4aSElliott Hughes        rsub = doc.statements[0].statements[0]
1400*e1fe3e4aSElliott Hughes        self.assertEqual(type(rsub), ast.ReverseChainSingleSubstStatement)
1401*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(rsub.old_prefix), "BACK TRACK")
1402*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(rsub.old_suffix), "LOOK AHEAD")
1403*e1fe3e4aSElliott Hughes        self.assertEqual(
1404*e1fe3e4aSElliott Hughes            mapping(rsub), {"a": "A.sc", "b": "B.sc", "c": "C.sc", "d": "D.sc"}
1405*e1fe3e4aSElliott Hughes        )
1406*e1fe3e4aSElliott Hughes
1407*e1fe3e4aSElliott Hughes    def test_rsub_from(self):
1408*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
1409*e1fe3e4aSElliott Hughes            FeatureLibError,
1410*e1fe3e4aSElliott Hughes            'Reverse chaining substitutions do not support "from"',
1411*e1fe3e4aSElliott Hughes            self.parse,
1412*e1fe3e4aSElliott Hughes            "feature test {rsub a from [a.1 a.2 a.3];} test;",
1413*e1fe3e4aSElliott Hughes        )
1414*e1fe3e4aSElliott Hughes
1415*e1fe3e4aSElliott Hughes    def test_rsub_nonsingle(self):
1416*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
1417*e1fe3e4aSElliott Hughes            FeatureLibError,
1418*e1fe3e4aSElliott Hughes            "In reverse chaining single substitutions, only a single glyph "
1419*e1fe3e4aSElliott Hughes            "or glyph class can be replaced",
1420*e1fe3e4aSElliott Hughes            self.parse,
1421*e1fe3e4aSElliott Hughes            "feature test {rsub c d by c_d;} test;",
1422*e1fe3e4aSElliott Hughes        )
1423*e1fe3e4aSElliott Hughes
1424*e1fe3e4aSElliott Hughes    def test_rsub_multiple_replacement_glyphs(self):
1425*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
1426*e1fe3e4aSElliott Hughes            FeatureLibError,
1427*e1fe3e4aSElliott Hughes            "In reverse chaining single substitutions, the replacement "
1428*e1fe3e4aSElliott Hughes            r'\(after "by"\) must be a single glyph or glyph class',
1429*e1fe3e4aSElliott Hughes            self.parse,
1430*e1fe3e4aSElliott Hughes            "feature test {rsub f_i by f i;} test;",
1431*e1fe3e4aSElliott Hughes        )
1432*e1fe3e4aSElliott Hughes
1433*e1fe3e4aSElliott Hughes    def test_script(self):
1434*e1fe3e4aSElliott Hughes        doc = self.parse("feature test {script cyrl;} test;")
1435*e1fe3e4aSElliott Hughes        s = doc.statements[0].statements[0]
1436*e1fe3e4aSElliott Hughes        self.assertEqual(type(s), ast.ScriptStatement)
1437*e1fe3e4aSElliott Hughes        self.assertEqual(s.script, "cyrl")
1438*e1fe3e4aSElliott Hughes
1439*e1fe3e4aSElliott Hughes    def test_script_dflt(self):
1440*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
1441*e1fe3e4aSElliott Hughes            FeatureLibError,
1442*e1fe3e4aSElliott Hughes            '"dflt" is not a valid script tag; use "DFLT" instead',
1443*e1fe3e4aSElliott Hughes            self.parse,
1444*e1fe3e4aSElliott Hughes            "feature test {script dflt;} test;",
1445*e1fe3e4aSElliott Hughes        )
1446*e1fe3e4aSElliott Hughes
1447*e1fe3e4aSElliott Hughes    def test_stat_design_axis(self):  # STAT DesignAxis
1448*e1fe3e4aSElliott Hughes        doc = self.parse(
1449*e1fe3e4aSElliott Hughes            "table STAT { DesignAxis opsz 0 " '{name "Optical Size";}; } STAT;'
1450*e1fe3e4aSElliott Hughes        )
1451*e1fe3e4aSElliott Hughes        da = doc.statements[0].statements[0]
1452*e1fe3e4aSElliott Hughes        self.assertIsInstance(da, ast.STATDesignAxisStatement)
1453*e1fe3e4aSElliott Hughes        self.assertEqual(da.tag, "opsz")
1454*e1fe3e4aSElliott Hughes        self.assertEqual(da.axisOrder, 0)
1455*e1fe3e4aSElliott Hughes        self.assertEqual(da.names[0].string, "Optical Size")
1456*e1fe3e4aSElliott Hughes
1457*e1fe3e4aSElliott Hughes    def test_stat_axis_value_format1(self):  # STAT AxisValue
1458*e1fe3e4aSElliott Hughes        doc = self.parse(
1459*e1fe3e4aSElliott Hughes            "table STAT { DesignAxis opsz 0 "
1460*e1fe3e4aSElliott Hughes            '{name "Optical Size";}; '
1461*e1fe3e4aSElliott Hughes            'AxisValue {location opsz 8; name "Caption";}; } '
1462*e1fe3e4aSElliott Hughes            "STAT;"
1463*e1fe3e4aSElliott Hughes        )
1464*e1fe3e4aSElliott Hughes        avr = doc.statements[0].statements[1]
1465*e1fe3e4aSElliott Hughes        self.assertIsInstance(avr, ast.STATAxisValueStatement)
1466*e1fe3e4aSElliott Hughes        self.assertEqual(avr.locations[0].tag, "opsz")
1467*e1fe3e4aSElliott Hughes        self.assertEqual(avr.locations[0].values[0], 8)
1468*e1fe3e4aSElliott Hughes        self.assertEqual(avr.names[0].string, "Caption")
1469*e1fe3e4aSElliott Hughes
1470*e1fe3e4aSElliott Hughes    def test_stat_axis_value_format2(self):  # STAT AxisValue
1471*e1fe3e4aSElliott Hughes        doc = self.parse(
1472*e1fe3e4aSElliott Hughes            "table STAT { DesignAxis opsz 0 "
1473*e1fe3e4aSElliott Hughes            '{name "Optical Size";}; '
1474*e1fe3e4aSElliott Hughes            'AxisValue {location opsz 8 6 10; name "Caption";}; } '
1475*e1fe3e4aSElliott Hughes            "STAT;"
1476*e1fe3e4aSElliott Hughes        )
1477*e1fe3e4aSElliott Hughes        avr = doc.statements[0].statements[1]
1478*e1fe3e4aSElliott Hughes        self.assertIsInstance(avr, ast.STATAxisValueStatement)
1479*e1fe3e4aSElliott Hughes        self.assertEqual(avr.locations[0].tag, "opsz")
1480*e1fe3e4aSElliott Hughes        self.assertEqual(avr.locations[0].values, [8, 6, 10])
1481*e1fe3e4aSElliott Hughes        self.assertEqual(avr.names[0].string, "Caption")
1482*e1fe3e4aSElliott Hughes
1483*e1fe3e4aSElliott Hughes    def test_stat_axis_value_format2_bad_range(self):  # STAT AxisValue
1484*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
1485*e1fe3e4aSElliott Hughes            FeatureLibError,
1486*e1fe3e4aSElliott Hughes            "Default value 5 is outside of specified range 6-10.",
1487*e1fe3e4aSElliott Hughes            self.parse,
1488*e1fe3e4aSElliott Hughes            "table STAT { DesignAxis opsz 0 "
1489*e1fe3e4aSElliott Hughes            '{name "Optical Size";}; '
1490*e1fe3e4aSElliott Hughes            'AxisValue {location opsz 5 6 10; name "Caption";}; } '
1491*e1fe3e4aSElliott Hughes            "STAT;",
1492*e1fe3e4aSElliott Hughes        )
1493*e1fe3e4aSElliott Hughes
1494*e1fe3e4aSElliott Hughes    def test_stat_axis_value_format4(self):  # STAT AxisValue
1495*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
1496*e1fe3e4aSElliott Hughes            FeatureLibError,
1497*e1fe3e4aSElliott Hughes            "Only one value is allowed in a Format 4 Axis Value Record, but 3 were found.",
1498*e1fe3e4aSElliott Hughes            self.parse,
1499*e1fe3e4aSElliott Hughes            "table STAT { "
1500*e1fe3e4aSElliott Hughes            'DesignAxis opsz 0 {name "Optical Size";}; '
1501*e1fe3e4aSElliott Hughes            'DesignAxis wdth 0 {name "Width";}; '
1502*e1fe3e4aSElliott Hughes            "AxisValue {"
1503*e1fe3e4aSElliott Hughes            "location opsz 8 6 10; "
1504*e1fe3e4aSElliott Hughes            "location wdth 400; "
1505*e1fe3e4aSElliott Hughes            'name "Caption";}; } '
1506*e1fe3e4aSElliott Hughes            "STAT;",
1507*e1fe3e4aSElliott Hughes        )
1508*e1fe3e4aSElliott Hughes
1509*e1fe3e4aSElliott Hughes    def test_stat_elidedfallbackname(self):  # STAT ElidedFallbackName
1510*e1fe3e4aSElliott Hughes        doc = self.parse(
1511*e1fe3e4aSElliott Hughes            'table STAT { ElidedFallbackName {name "Roman"; '
1512*e1fe3e4aSElliott Hughes            'name 3 1 0x0411 "ローマン"; }; '
1513*e1fe3e4aSElliott Hughes            "} STAT;"
1514*e1fe3e4aSElliott Hughes        )
1515*e1fe3e4aSElliott Hughes        nameRecord = doc.statements[0].statements[0]
1516*e1fe3e4aSElliott Hughes        self.assertIsInstance(nameRecord, ast.ElidedFallbackName)
1517*e1fe3e4aSElliott Hughes        self.assertEqual(nameRecord.names[0].string, "Roman")
1518*e1fe3e4aSElliott Hughes        self.assertEqual(nameRecord.names[1].string, "ローマン")
1519*e1fe3e4aSElliott Hughes
1520*e1fe3e4aSElliott Hughes    def test_stat_elidedfallbacknameid(self):  # STAT ElidedFallbackNameID
1521*e1fe3e4aSElliott Hughes        doc = self.parse(
1522*e1fe3e4aSElliott Hughes            'table name { nameid 278 "Roman"; } name; '
1523*e1fe3e4aSElliott Hughes            "table STAT { ElidedFallbackNameID 278; "
1524*e1fe3e4aSElliott Hughes            "} STAT;"
1525*e1fe3e4aSElliott Hughes        )
1526*e1fe3e4aSElliott Hughes        nameRecord = doc.statements[0].statements[0]
1527*e1fe3e4aSElliott Hughes        self.assertIsInstance(nameRecord, ast.NameRecord)
1528*e1fe3e4aSElliott Hughes        self.assertEqual(nameRecord.string, "Roman")
1529*e1fe3e4aSElliott Hughes
1530*e1fe3e4aSElliott Hughes    def test_sub_single_format_a(self):  # GSUB LookupType 1
1531*e1fe3e4aSElliott Hughes        doc = self.parse("feature smcp {substitute a by a.sc;} smcp;")
1532*e1fe3e4aSElliott Hughes        sub = doc.statements[0].statements[0]
1533*e1fe3e4aSElliott Hughes        self.assertIsInstance(sub, ast.SingleSubstStatement)
1534*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(sub.prefix), "")
1535*e1fe3e4aSElliott Hughes        self.assertEqual(mapping(sub), {"a": "a.sc"})
1536*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(sub.suffix), "")
1537*e1fe3e4aSElliott Hughes
1538*e1fe3e4aSElliott Hughes    def test_sub_single_format_a_chained(self):  # chain to GSUB LookupType 1
1539*e1fe3e4aSElliott Hughes        doc = self.parse("feature test {sub [A a] d' [C] by d.alt;} test;")
1540*e1fe3e4aSElliott Hughes        sub = doc.statements[0].statements[0]
1541*e1fe3e4aSElliott Hughes        self.assertIsInstance(sub, ast.SingleSubstStatement)
1542*e1fe3e4aSElliott Hughes        self.assertEqual(mapping(sub), {"d": "d.alt"})
1543*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(sub.prefix), "[A a]")
1544*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(sub.suffix), "C")
1545*e1fe3e4aSElliott Hughes
1546*e1fe3e4aSElliott Hughes    def test_sub_single_format_a_cid(self):  # GSUB LookupType 1
1547*e1fe3e4aSElliott Hughes        doc = self.parse(r"feature smcp {substitute \12345 by \78987;} smcp;")
1548*e1fe3e4aSElliott Hughes        sub = doc.statements[0].statements[0]
1549*e1fe3e4aSElliott Hughes        self.assertIsInstance(sub, ast.SingleSubstStatement)
1550*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(sub.prefix), "")
1551*e1fe3e4aSElliott Hughes        self.assertEqual(mapping(sub), {"cid12345": "cid78987"})
1552*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(sub.suffix), "")
1553*e1fe3e4aSElliott Hughes
1554*e1fe3e4aSElliott Hughes    def test_sub_single_format_b(self):  # GSUB LookupType 1
1555*e1fe3e4aSElliott Hughes        doc = self.parse(
1556*e1fe3e4aSElliott Hughes            "feature smcp {"
1557*e1fe3e4aSElliott Hughes            "    substitute [one.fitted one.oldstyle] by one;"
1558*e1fe3e4aSElliott Hughes            "} smcp;"
1559*e1fe3e4aSElliott Hughes        )
1560*e1fe3e4aSElliott Hughes        sub = doc.statements[0].statements[0]
1561*e1fe3e4aSElliott Hughes        self.assertIsInstance(sub, ast.SingleSubstStatement)
1562*e1fe3e4aSElliott Hughes        self.assertEqual(mapping(sub), {"one.fitted": "one", "one.oldstyle": "one"})
1563*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(sub.prefix), "")
1564*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(sub.suffix), "")
1565*e1fe3e4aSElliott Hughes
1566*e1fe3e4aSElliott Hughes    def test_sub_single_format_b_chained(self):  # chain to GSUB LookupType 1
1567*e1fe3e4aSElliott Hughes        doc = self.parse(
1568*e1fe3e4aSElliott Hughes            "feature smcp {"
1569*e1fe3e4aSElliott Hughes            "    substitute PRE FIX [one.fitted one.oldstyle]' SUF FIX by one;"
1570*e1fe3e4aSElliott Hughes            "} smcp;"
1571*e1fe3e4aSElliott Hughes        )
1572*e1fe3e4aSElliott Hughes        sub = doc.statements[0].statements[0]
1573*e1fe3e4aSElliott Hughes        self.assertIsInstance(sub, ast.SingleSubstStatement)
1574*e1fe3e4aSElliott Hughes        self.assertEqual(mapping(sub), {"one.fitted": "one", "one.oldstyle": "one"})
1575*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(sub.prefix), "PRE FIX")
1576*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(sub.suffix), "SUF FIX")
1577*e1fe3e4aSElliott Hughes
1578*e1fe3e4aSElliott Hughes    def test_sub_single_format_c(self):  # GSUB LookupType 1
1579*e1fe3e4aSElliott Hughes        doc = self.parse(
1580*e1fe3e4aSElliott Hughes            "feature smcp {" "    substitute [a-d] by [A.sc-D.sc];" "} smcp;"
1581*e1fe3e4aSElliott Hughes        )
1582*e1fe3e4aSElliott Hughes        sub = doc.statements[0].statements[0]
1583*e1fe3e4aSElliott Hughes        self.assertIsInstance(sub, ast.SingleSubstStatement)
1584*e1fe3e4aSElliott Hughes        self.assertEqual(
1585*e1fe3e4aSElliott Hughes            mapping(sub), {"a": "A.sc", "b": "B.sc", "c": "C.sc", "d": "D.sc"}
1586*e1fe3e4aSElliott Hughes        )
1587*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(sub.prefix), "")
1588*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(sub.suffix), "")
1589*e1fe3e4aSElliott Hughes
1590*e1fe3e4aSElliott Hughes    def test_sub_single_format_c_chained(self):  # chain to GSUB LookupType 1
1591*e1fe3e4aSElliott Hughes        doc = self.parse(
1592*e1fe3e4aSElliott Hughes            "feature smcp {" "    substitute [a-d]' X Y [Z z] by [A.sc-D.sc];" "} smcp;"
1593*e1fe3e4aSElliott Hughes        )
1594*e1fe3e4aSElliott Hughes        sub = doc.statements[0].statements[0]
1595*e1fe3e4aSElliott Hughes        self.assertIsInstance(sub, ast.SingleSubstStatement)
1596*e1fe3e4aSElliott Hughes        self.assertEqual(
1597*e1fe3e4aSElliott Hughes            mapping(sub), {"a": "A.sc", "b": "B.sc", "c": "C.sc", "d": "D.sc"}
1598*e1fe3e4aSElliott Hughes        )
1599*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(sub.prefix), "")
1600*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(sub.suffix), "X Y [Z z]")
1601*e1fe3e4aSElliott Hughes
1602*e1fe3e4aSElliott Hughes    def test_sub_single_format_c_different_num_elements(self):
1603*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
1604*e1fe3e4aSElliott Hughes            FeatureLibError,
1605*e1fe3e4aSElliott Hughes            'Expected a glyph class with 4 elements after "by", '
1606*e1fe3e4aSElliott Hughes            "but found a glyph class with 26 elements",
1607*e1fe3e4aSElliott Hughes            self.parse,
1608*e1fe3e4aSElliott Hughes            "feature smcp {sub [a-d] by [A.sc-Z.sc];} smcp;",
1609*e1fe3e4aSElliott Hughes        )
1610*e1fe3e4aSElliott Hughes
1611*e1fe3e4aSElliott Hughes    def test_sub_with_values(self):
1612*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
1613*e1fe3e4aSElliott Hughes            FeatureLibError,
1614*e1fe3e4aSElliott Hughes            "Substitution statements cannot contain values",
1615*e1fe3e4aSElliott Hughes            self.parse,
1616*e1fe3e4aSElliott Hughes            "feature smcp {sub A' 20 by A.sc;} smcp;",
1617*e1fe3e4aSElliott Hughes        )
1618*e1fe3e4aSElliott Hughes
1619*e1fe3e4aSElliott Hughes    def test_substitute_multiple(self):  # GSUB LookupType 2
1620*e1fe3e4aSElliott Hughes        doc = self.parse("lookup Look {substitute f_f_i by f f i;} Look;")
1621*e1fe3e4aSElliott Hughes        sub = doc.statements[0].statements[0]
1622*e1fe3e4aSElliott Hughes        self.assertIsInstance(sub, ast.MultipleSubstStatement)
1623*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([sub.glyph]), "f_f_i")
1624*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(sub.replacement), "f f i")
1625*e1fe3e4aSElliott Hughes
1626*e1fe3e4aSElliott Hughes    def test_substitute_multiple_chained(self):  # chain to GSUB LookupType 2
1627*e1fe3e4aSElliott Hughes        doc = self.parse("lookup L {sub [A-C] f_f_i' [X-Z] by f f i;} L;")
1628*e1fe3e4aSElliott Hughes        sub = doc.statements[0].statements[0]
1629*e1fe3e4aSElliott Hughes        self.assertIsInstance(sub, ast.MultipleSubstStatement)
1630*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([sub.glyph]), "f_f_i")
1631*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(sub.replacement), "f f i")
1632*e1fe3e4aSElliott Hughes
1633*e1fe3e4aSElliott Hughes    def test_substitute_multiple_force_chained(self):
1634*e1fe3e4aSElliott Hughes        doc = self.parse("lookup L {sub f_f_i' by f f i;} L;")
1635*e1fe3e4aSElliott Hughes        sub = doc.statements[0].statements[0]
1636*e1fe3e4aSElliott Hughes        self.assertIsInstance(sub, ast.MultipleSubstStatement)
1637*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([sub.glyph]), "f_f_i")
1638*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(sub.replacement), "f f i")
1639*e1fe3e4aSElliott Hughes        self.assertEqual(sub.asFea(), "sub f_f_i' by f f i;")
1640*e1fe3e4aSElliott Hughes
1641*e1fe3e4aSElliott Hughes    def test_substitute_multiple_classes(self):
1642*e1fe3e4aSElliott Hughes        doc = self.parse("lookup Look {substitute [f_i f_l] by [f f] [i l];} Look;")
1643*e1fe3e4aSElliott Hughes        sub = doc.statements[0].statements[0]
1644*e1fe3e4aSElliott Hughes        self.assertIsInstance(sub, ast.MultipleSubstStatement)
1645*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([sub.glyph]), "[f_i f_l]")
1646*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(sub.replacement), "[f f] [i l]")
1647*e1fe3e4aSElliott Hughes
1648*e1fe3e4aSElliott Hughes    def test_substitute_multiple_classes_mixed(self):
1649*e1fe3e4aSElliott Hughes        doc = self.parse("lookup Look {substitute [f_i f_l] by f [i l];} Look;")
1650*e1fe3e4aSElliott Hughes        sub = doc.statements[0].statements[0]
1651*e1fe3e4aSElliott Hughes        self.assertIsInstance(sub, ast.MultipleSubstStatement)
1652*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([sub.glyph]), "[f_i f_l]")
1653*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(sub.replacement), "f [i l]")
1654*e1fe3e4aSElliott Hughes
1655*e1fe3e4aSElliott Hughes    def test_substitute_multiple_classes_mixed_singleton(self):
1656*e1fe3e4aSElliott Hughes        doc = self.parse("lookup Look {substitute [f_i f_l] by [f] [i l];} Look;")
1657*e1fe3e4aSElliott Hughes        sub = doc.statements[0].statements[0]
1658*e1fe3e4aSElliott Hughes        self.assertIsInstance(sub, ast.MultipleSubstStatement)
1659*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([sub.glyph]), "[f_i f_l]")
1660*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(sub.replacement), "f [i l]")
1661*e1fe3e4aSElliott Hughes
1662*e1fe3e4aSElliott Hughes    def test_substitute_multiple_classes_mismatch(self):
1663*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
1664*e1fe3e4aSElliott Hughes            FeatureLibError,
1665*e1fe3e4aSElliott Hughes            'Expected a glyph class with 1 or 3 elements after "by", '
1666*e1fe3e4aSElliott Hughes            "but found a glyph class with 2 elements",
1667*e1fe3e4aSElliott Hughes            self.parse,
1668*e1fe3e4aSElliott Hughes            "lookup Look {substitute [f_i f_l f_f_i] by [f f_f] [i l i];} Look;",
1669*e1fe3e4aSElliott Hughes        )
1670*e1fe3e4aSElliott Hughes
1671*e1fe3e4aSElliott Hughes    def test_substitute_multiple_by_mutliple(self):
1672*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
1673*e1fe3e4aSElliott Hughes            FeatureLibError,
1674*e1fe3e4aSElliott Hughes            "Direct substitution of multiple glyphs by multiple glyphs "
1675*e1fe3e4aSElliott Hughes            "is not supported",
1676*e1fe3e4aSElliott Hughes            self.parse,
1677*e1fe3e4aSElliott Hughes            "lookup MxM {sub a b c by d e f;} MxM;",
1678*e1fe3e4aSElliott Hughes        )
1679*e1fe3e4aSElliott Hughes
1680*e1fe3e4aSElliott Hughes    def test_split_marked_glyphs_runs(self):
1681*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
1682*e1fe3e4aSElliott Hughes            FeatureLibError,
1683*e1fe3e4aSElliott Hughes            "Unsupported contextual target sequence",
1684*e1fe3e4aSElliott Hughes            self.parse,
1685*e1fe3e4aSElliott Hughes            "feature test{" "    ignore pos a' x x A';" "} test;",
1686*e1fe3e4aSElliott Hughes        )
1687*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
1688*e1fe3e4aSElliott Hughes            FeatureLibError,
1689*e1fe3e4aSElliott Hughes            "Unsupported contextual target sequence",
1690*e1fe3e4aSElliott Hughes            self.parse,
1691*e1fe3e4aSElliott Hughes            "lookup shift {"
1692*e1fe3e4aSElliott Hughes            "    pos a <0 -10 0 0>;"
1693*e1fe3e4aSElliott Hughes            "    pos A <0 10 0 0>;"
1694*e1fe3e4aSElliott Hughes            "} shift;"
1695*e1fe3e4aSElliott Hughes            "feature test {"
1696*e1fe3e4aSElliott Hughes            "    sub a' lookup shift x x A' lookup shift;"
1697*e1fe3e4aSElliott Hughes            "} test;",
1698*e1fe3e4aSElliott Hughes        )
1699*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
1700*e1fe3e4aSElliott Hughes            FeatureLibError,
1701*e1fe3e4aSElliott Hughes            "Unsupported contextual target sequence",
1702*e1fe3e4aSElliott Hughes            self.parse,
1703*e1fe3e4aSElliott Hughes            "feature test {" "    ignore sub a' x x A';" "} test;",
1704*e1fe3e4aSElliott Hughes        )
1705*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
1706*e1fe3e4aSElliott Hughes            FeatureLibError,
1707*e1fe3e4aSElliott Hughes            "Unsupported contextual target sequence",
1708*e1fe3e4aSElliott Hughes            self.parse,
1709*e1fe3e4aSElliott Hughes            "lookup upper {"
1710*e1fe3e4aSElliott Hughes            "    sub a by A;"
1711*e1fe3e4aSElliott Hughes            "} upper;"
1712*e1fe3e4aSElliott Hughes            "lookup lower {"
1713*e1fe3e4aSElliott Hughes            "    sub A by a;"
1714*e1fe3e4aSElliott Hughes            "} lower;"
1715*e1fe3e4aSElliott Hughes            "feature test {"
1716*e1fe3e4aSElliott Hughes            "    sub a' lookup upper x x A' lookup lower;"
1717*e1fe3e4aSElliott Hughes            "} test;",
1718*e1fe3e4aSElliott Hughes        )
1719*e1fe3e4aSElliott Hughes
1720*e1fe3e4aSElliott Hughes    def test_substitute_mix_single_multiple(self):
1721*e1fe3e4aSElliott Hughes        doc = self.parse(
1722*e1fe3e4aSElliott Hughes            "lookup Look {"
1723*e1fe3e4aSElliott Hughes            "  sub f_f   by f f;"
1724*e1fe3e4aSElliott Hughes            "  sub f     by f;"
1725*e1fe3e4aSElliott Hughes            "  sub f_f_i by f f i;"
1726*e1fe3e4aSElliott Hughes            "  sub [a a.sc] by a;"
1727*e1fe3e4aSElliott Hughes            "  sub [a a.sc] by [b b.sc];"
1728*e1fe3e4aSElliott Hughes            "} Look;"
1729*e1fe3e4aSElliott Hughes        )
1730*e1fe3e4aSElliott Hughes        statements = doc.statements[0].statements
1731*e1fe3e4aSElliott Hughes        for sub in statements:
1732*e1fe3e4aSElliott Hughes            self.assertIsInstance(sub, ast.MultipleSubstStatement)
1733*e1fe3e4aSElliott Hughes        self.assertEqual(statements[1].glyph, "f")
1734*e1fe3e4aSElliott Hughes        self.assertEqual(statements[1].replacement, ["f"])
1735*e1fe3e4aSElliott Hughes        self.assertEqual(statements[3].glyph, "a")
1736*e1fe3e4aSElliott Hughes        self.assertEqual(statements[3].replacement, ["a"])
1737*e1fe3e4aSElliott Hughes        self.assertEqual(statements[4].glyph, "a.sc")
1738*e1fe3e4aSElliott Hughes        self.assertEqual(statements[4].replacement, ["a"])
1739*e1fe3e4aSElliott Hughes        self.assertEqual(statements[5].glyph, "a")
1740*e1fe3e4aSElliott Hughes        self.assertEqual(statements[5].replacement, ["b"])
1741*e1fe3e4aSElliott Hughes        self.assertEqual(statements[6].glyph, "a.sc")
1742*e1fe3e4aSElliott Hughes        self.assertEqual(statements[6].replacement, ["b.sc"])
1743*e1fe3e4aSElliott Hughes
1744*e1fe3e4aSElliott Hughes    def test_substitute_from(self):  # GSUB LookupType 3
1745*e1fe3e4aSElliott Hughes        doc = self.parse(
1746*e1fe3e4aSElliott Hughes            "feature test {" "  substitute a from [a.1 a.2 a.3];" "} test;"
1747*e1fe3e4aSElliott Hughes        )
1748*e1fe3e4aSElliott Hughes        sub = doc.statements[0].statements[0]
1749*e1fe3e4aSElliott Hughes        self.assertIsInstance(sub, ast.AlternateSubstStatement)
1750*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(sub.prefix), "")
1751*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([sub.glyph]), "a")
1752*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(sub.suffix), "")
1753*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([sub.replacement]), "[a.1 a.2 a.3]")
1754*e1fe3e4aSElliott Hughes
1755*e1fe3e4aSElliott Hughes    def test_substitute_from_chained(self):  # chain to GSUB LookupType 3
1756*e1fe3e4aSElliott Hughes        doc = self.parse(
1757*e1fe3e4aSElliott Hughes            "feature test {" "  substitute A B a' [Y y] Z from [a.1 a.2 a.3];" "} test;"
1758*e1fe3e4aSElliott Hughes        )
1759*e1fe3e4aSElliott Hughes        sub = doc.statements[0].statements[0]
1760*e1fe3e4aSElliott Hughes        self.assertIsInstance(sub, ast.AlternateSubstStatement)
1761*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(sub.prefix), "A B")
1762*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([sub.glyph]), "a")
1763*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(sub.suffix), "[Y y] Z")
1764*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([sub.replacement]), "[a.1 a.2 a.3]")
1765*e1fe3e4aSElliott Hughes
1766*e1fe3e4aSElliott Hughes    def test_substitute_from_cid(self):  # GSUB LookupType 3
1767*e1fe3e4aSElliott Hughes        doc = self.parse(
1768*e1fe3e4aSElliott Hughes            r"feature test {" r"  substitute \7 from [\111 \222];" r"} test;"
1769*e1fe3e4aSElliott Hughes        )
1770*e1fe3e4aSElliott Hughes        sub = doc.statements[0].statements[0]
1771*e1fe3e4aSElliott Hughes        self.assertIsInstance(sub, ast.AlternateSubstStatement)
1772*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(sub.prefix), "")
1773*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([sub.glyph]), "cid00007")
1774*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(sub.suffix), "")
1775*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([sub.replacement]), "[cid00111 cid00222]")
1776*e1fe3e4aSElliott Hughes
1777*e1fe3e4aSElliott Hughes    def test_substitute_from_glyphclass(self):  # GSUB LookupType 3
1778*e1fe3e4aSElliott Hughes        doc = self.parse(
1779*e1fe3e4aSElliott Hughes            "feature test {"
1780*e1fe3e4aSElliott Hughes            "  @Ampersands = [ampersand.1 ampersand.2];"
1781*e1fe3e4aSElliott Hughes            "  substitute ampersand from @Ampersands;"
1782*e1fe3e4aSElliott Hughes            "} test;"
1783*e1fe3e4aSElliott Hughes        )
1784*e1fe3e4aSElliott Hughes        [glyphclass, sub] = doc.statements[0].statements
1785*e1fe3e4aSElliott Hughes        self.assertIsInstance(sub, ast.AlternateSubstStatement)
1786*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(sub.prefix), "")
1787*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([sub.glyph]), "ampersand")
1788*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(sub.suffix), "")
1789*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr([sub.replacement]), "[ampersand.1 ampersand.2]")
1790*e1fe3e4aSElliott Hughes
1791*e1fe3e4aSElliott Hughes    def test_substitute_ligature(self):  # GSUB LookupType 4
1792*e1fe3e4aSElliott Hughes        doc = self.parse("feature liga {substitute f f i by f_f_i;} liga;")
1793*e1fe3e4aSElliott Hughes        sub = doc.statements[0].statements[0]
1794*e1fe3e4aSElliott Hughes        self.assertIsInstance(sub, ast.LigatureSubstStatement)
1795*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(sub.glyphs), "f f i")
1796*e1fe3e4aSElliott Hughes        self.assertEqual(sub.replacement, "f_f_i")
1797*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(sub.prefix), "")
1798*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(sub.suffix), "")
1799*e1fe3e4aSElliott Hughes
1800*e1fe3e4aSElliott Hughes    def test_substitute_ligature_chained(self):  # chain to GSUB LookupType 4
1801*e1fe3e4aSElliott Hughes        doc = self.parse("feature F {substitute A B f' i' Z by f_i;} F;")
1802*e1fe3e4aSElliott Hughes        sub = doc.statements[0].statements[0]
1803*e1fe3e4aSElliott Hughes        self.assertIsInstance(sub, ast.LigatureSubstStatement)
1804*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(sub.glyphs), "f i")
1805*e1fe3e4aSElliott Hughes        self.assertEqual(sub.replacement, "f_i")
1806*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(sub.prefix), "A B")
1807*e1fe3e4aSElliott Hughes        self.assertEqual(glyphstr(sub.suffix), "Z")
1808*e1fe3e4aSElliott Hughes
1809*e1fe3e4aSElliott Hughes    def test_substitute_lookups(self):  # GSUB LookupType 6
1810*e1fe3e4aSElliott Hughes        doc = Parser(self.getpath("spec5fi1.fea"), GLYPHNAMES).parse()
1811*e1fe3e4aSElliott Hughes        [_, _, _, langsys, ligs, sub, feature] = doc.statements
1812*e1fe3e4aSElliott Hughes        self.assertEqual(feature.statements[0].lookups, [[ligs], None, [sub]])
1813*e1fe3e4aSElliott Hughes        self.assertEqual(feature.statements[1].lookups, [[ligs], None, [sub]])
1814*e1fe3e4aSElliott Hughes
1815*e1fe3e4aSElliott Hughes    def test_substitute_missing_by(self):
1816*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
1817*e1fe3e4aSElliott Hughes            FeatureLibError,
1818*e1fe3e4aSElliott Hughes            'Expected "by", "from" or explicit lookup references',
1819*e1fe3e4aSElliott Hughes            self.parse,
1820*e1fe3e4aSElliott Hughes            "feature liga {substitute f f i;} liga;",
1821*e1fe3e4aSElliott Hughes        )
1822*e1fe3e4aSElliott Hughes
1823*e1fe3e4aSElliott Hughes    def test_substitute_invalid_statement(self):
1824*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
1825*e1fe3e4aSElliott Hughes            FeatureLibError,
1826*e1fe3e4aSElliott Hughes            "Invalid substitution statement",
1827*e1fe3e4aSElliott Hughes            Parser(self.getpath("GSUB_error.fea"), GLYPHNAMES).parse,
1828*e1fe3e4aSElliott Hughes        )
1829*e1fe3e4aSElliott Hughes
1830*e1fe3e4aSElliott Hughes    def test_subtable(self):
1831*e1fe3e4aSElliott Hughes        doc = self.parse("feature test {subtable;} test;")
1832*e1fe3e4aSElliott Hughes        s = doc.statements[0].statements[0]
1833*e1fe3e4aSElliott Hughes        self.assertIsInstance(s, ast.SubtableStatement)
1834*e1fe3e4aSElliott Hughes
1835*e1fe3e4aSElliott Hughes    def test_table_badEnd(self):
1836*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
1837*e1fe3e4aSElliott Hughes            FeatureLibError,
1838*e1fe3e4aSElliott Hughes            'Expected "GDEF"',
1839*e1fe3e4aSElliott Hughes            self.parse,
1840*e1fe3e4aSElliott Hughes            "table GDEF {LigatureCaretByPos f_i 400;} ABCD;",
1841*e1fe3e4aSElliott Hughes        )
1842*e1fe3e4aSElliott Hughes
1843*e1fe3e4aSElliott Hughes    def test_table_comment(self):
1844*e1fe3e4aSElliott Hughes        for table in "BASE GDEF OS/2 head hhea name vhea".split():
1845*e1fe3e4aSElliott Hughes            doc = self.parse("table %s { # Comment\n } %s;" % (table, table))
1846*e1fe3e4aSElliott Hughes            comment = doc.statements[0].statements[0]
1847*e1fe3e4aSElliott Hughes            self.assertIsInstance(comment, ast.Comment)
1848*e1fe3e4aSElliott Hughes            self.assertEqual(comment.text, "# Comment")
1849*e1fe3e4aSElliott Hughes
1850*e1fe3e4aSElliott Hughes    def test_table_unsupported(self):
1851*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
1852*e1fe3e4aSElliott Hughes            FeatureLibError,
1853*e1fe3e4aSElliott Hughes            '"table Foo" is not supported',
1854*e1fe3e4aSElliott Hughes            self.parse,
1855*e1fe3e4aSElliott Hughes            "table Foo {LigatureCaretByPos f_i 400;} Foo;",
1856*e1fe3e4aSElliott Hughes        )
1857*e1fe3e4aSElliott Hughes
1858*e1fe3e4aSElliott Hughes    def test_valuerecord_format_a_horizontal(self):
1859*e1fe3e4aSElliott Hughes        doc = self.parse("feature liga {valueRecordDef 123 foo;} liga;")
1860*e1fe3e4aSElliott Hughes        valuedef = doc.statements[0].statements[0]
1861*e1fe3e4aSElliott Hughes        value = valuedef.value
1862*e1fe3e4aSElliott Hughes        self.assertIsNone(value.xPlacement)
1863*e1fe3e4aSElliott Hughes        self.assertIsNone(value.yPlacement)
1864*e1fe3e4aSElliott Hughes        self.assertEqual(value.xAdvance, 123)
1865*e1fe3e4aSElliott Hughes        self.assertIsNone(value.yAdvance)
1866*e1fe3e4aSElliott Hughes        self.assertIsNone(value.xPlaDevice)
1867*e1fe3e4aSElliott Hughes        self.assertIsNone(value.yPlaDevice)
1868*e1fe3e4aSElliott Hughes        self.assertIsNone(value.xAdvDevice)
1869*e1fe3e4aSElliott Hughes        self.assertIsNone(value.yAdvDevice)
1870*e1fe3e4aSElliott Hughes        self.assertEqual(valuedef.asFea(), "valueRecordDef 123 foo;")
1871*e1fe3e4aSElliott Hughes        self.assertEqual(value.asFea(), "123")
1872*e1fe3e4aSElliott Hughes
1873*e1fe3e4aSElliott Hughes    def test_valuerecord_format_a_vertical(self):
1874*e1fe3e4aSElliott Hughes        doc = self.parse("feature vkrn {valueRecordDef 123 foo;} vkrn;")
1875*e1fe3e4aSElliott Hughes        valuedef = doc.statements[0].statements[0]
1876*e1fe3e4aSElliott Hughes        value = valuedef.value
1877*e1fe3e4aSElliott Hughes        self.assertIsNone(value.xPlacement)
1878*e1fe3e4aSElliott Hughes        self.assertIsNone(value.yPlacement)
1879*e1fe3e4aSElliott Hughes        self.assertIsNone(value.xAdvance)
1880*e1fe3e4aSElliott Hughes        self.assertEqual(value.yAdvance, 123)
1881*e1fe3e4aSElliott Hughes        self.assertIsNone(value.xPlaDevice)
1882*e1fe3e4aSElliott Hughes        self.assertIsNone(value.yPlaDevice)
1883*e1fe3e4aSElliott Hughes        self.assertIsNone(value.xAdvDevice)
1884*e1fe3e4aSElliott Hughes        self.assertIsNone(value.yAdvDevice)
1885*e1fe3e4aSElliott Hughes        self.assertEqual(valuedef.asFea(), "valueRecordDef 123 foo;")
1886*e1fe3e4aSElliott Hughes        self.assertEqual(value.asFea(), "123")
1887*e1fe3e4aSElliott Hughes
1888*e1fe3e4aSElliott Hughes    def test_valuerecord_format_a_zero_horizontal(self):
1889*e1fe3e4aSElliott Hughes        doc = self.parse("feature liga {valueRecordDef 0 foo;} liga;")
1890*e1fe3e4aSElliott Hughes        valuedef = doc.statements[0].statements[0]
1891*e1fe3e4aSElliott Hughes        value = valuedef.value
1892*e1fe3e4aSElliott Hughes        self.assertIsNone(value.xPlacement)
1893*e1fe3e4aSElliott Hughes        self.assertIsNone(value.yPlacement)
1894*e1fe3e4aSElliott Hughes        self.assertEqual(value.xAdvance, 0)
1895*e1fe3e4aSElliott Hughes        self.assertIsNone(value.yAdvance)
1896*e1fe3e4aSElliott Hughes        self.assertIsNone(value.xPlaDevice)
1897*e1fe3e4aSElliott Hughes        self.assertIsNone(value.yPlaDevice)
1898*e1fe3e4aSElliott Hughes        self.assertIsNone(value.xAdvDevice)
1899*e1fe3e4aSElliott Hughes        self.assertIsNone(value.yAdvDevice)
1900*e1fe3e4aSElliott Hughes        self.assertEqual(valuedef.asFea(), "valueRecordDef 0 foo;")
1901*e1fe3e4aSElliott Hughes        self.assertEqual(value.asFea(), "0")
1902*e1fe3e4aSElliott Hughes
1903*e1fe3e4aSElliott Hughes    def test_valuerecord_format_a_zero_vertical(self):
1904*e1fe3e4aSElliott Hughes        doc = self.parse("feature vkrn {valueRecordDef 0 foo;} vkrn;")
1905*e1fe3e4aSElliott Hughes        valuedef = doc.statements[0].statements[0]
1906*e1fe3e4aSElliott Hughes        value = valuedef.value
1907*e1fe3e4aSElliott Hughes        self.assertIsNone(value.xPlacement)
1908*e1fe3e4aSElliott Hughes        self.assertIsNone(value.yPlacement)
1909*e1fe3e4aSElliott Hughes        self.assertIsNone(value.xAdvance)
1910*e1fe3e4aSElliott Hughes        self.assertEqual(value.yAdvance, 0)
1911*e1fe3e4aSElliott Hughes        self.assertIsNone(value.xPlaDevice)
1912*e1fe3e4aSElliott Hughes        self.assertIsNone(value.yPlaDevice)
1913*e1fe3e4aSElliott Hughes        self.assertIsNone(value.xAdvDevice)
1914*e1fe3e4aSElliott Hughes        self.assertIsNone(value.yAdvDevice)
1915*e1fe3e4aSElliott Hughes        self.assertEqual(valuedef.asFea(), "valueRecordDef 0 foo;")
1916*e1fe3e4aSElliott Hughes        self.assertEqual(value.asFea(), "0")
1917*e1fe3e4aSElliott Hughes
1918*e1fe3e4aSElliott Hughes    def test_valuerecord_format_a_vertical_contexts_(self):
1919*e1fe3e4aSElliott Hughes        for tag in "vkrn vpal vhal valt".split():
1920*e1fe3e4aSElliott Hughes            doc = self.parse("feature %s {valueRecordDef 77 foo;} %s;" % (tag, tag))
1921*e1fe3e4aSElliott Hughes            value = doc.statements[0].statements[0].value
1922*e1fe3e4aSElliott Hughes            if value.yAdvance != 77:
1923*e1fe3e4aSElliott Hughes                self.fail(
1924*e1fe3e4aSElliott Hughes                    msg="feature %s should be a vertical context "
1925*e1fe3e4aSElliott Hughes                    "for ValueRecord format A" % tag
1926*e1fe3e4aSElliott Hughes                )
1927*e1fe3e4aSElliott Hughes
1928*e1fe3e4aSElliott Hughes    def test_valuerecord_format_b(self):
1929*e1fe3e4aSElliott Hughes        doc = self.parse("feature liga {valueRecordDef <1 2 3 4> foo;} liga;")
1930*e1fe3e4aSElliott Hughes        valuedef = doc.statements[0].statements[0]
1931*e1fe3e4aSElliott Hughes        value = valuedef.value
1932*e1fe3e4aSElliott Hughes        self.assertEqual(value.xPlacement, 1)
1933*e1fe3e4aSElliott Hughes        self.assertEqual(value.yPlacement, 2)
1934*e1fe3e4aSElliott Hughes        self.assertEqual(value.xAdvance, 3)
1935*e1fe3e4aSElliott Hughes        self.assertEqual(value.yAdvance, 4)
1936*e1fe3e4aSElliott Hughes        self.assertIsNone(value.xPlaDevice)
1937*e1fe3e4aSElliott Hughes        self.assertIsNone(value.yPlaDevice)
1938*e1fe3e4aSElliott Hughes        self.assertIsNone(value.xAdvDevice)
1939*e1fe3e4aSElliott Hughes        self.assertIsNone(value.yAdvDevice)
1940*e1fe3e4aSElliott Hughes        self.assertEqual(valuedef.asFea(), "valueRecordDef <1 2 3 4> foo;")
1941*e1fe3e4aSElliott Hughes        self.assertEqual(value.asFea(), "<1 2 3 4>")
1942*e1fe3e4aSElliott Hughes
1943*e1fe3e4aSElliott Hughes    def test_valuerecord_format_b_zero(self):
1944*e1fe3e4aSElliott Hughes        doc = self.parse("feature liga {valueRecordDef <0 0 0 0> foo;} liga;")
1945*e1fe3e4aSElliott Hughes        valuedef = doc.statements[0].statements[0]
1946*e1fe3e4aSElliott Hughes        value = valuedef.value
1947*e1fe3e4aSElliott Hughes        self.assertEqual(value.xPlacement, 0)
1948*e1fe3e4aSElliott Hughes        self.assertEqual(value.yPlacement, 0)
1949*e1fe3e4aSElliott Hughes        self.assertEqual(value.xAdvance, 0)
1950*e1fe3e4aSElliott Hughes        self.assertEqual(value.yAdvance, 0)
1951*e1fe3e4aSElliott Hughes        self.assertIsNone(value.xPlaDevice)
1952*e1fe3e4aSElliott Hughes        self.assertIsNone(value.yPlaDevice)
1953*e1fe3e4aSElliott Hughes        self.assertIsNone(value.xAdvDevice)
1954*e1fe3e4aSElliott Hughes        self.assertIsNone(value.yAdvDevice)
1955*e1fe3e4aSElliott Hughes        self.assertEqual(valuedef.asFea(), "valueRecordDef <0 0 0 0> foo;")
1956*e1fe3e4aSElliott Hughes        self.assertEqual(value.asFea(), "<0 0 0 0>")
1957*e1fe3e4aSElliott Hughes
1958*e1fe3e4aSElliott Hughes    def test_valuerecord_format_c(self):
1959*e1fe3e4aSElliott Hughes        doc = self.parse(
1960*e1fe3e4aSElliott Hughes            "feature liga {"
1961*e1fe3e4aSElliott Hughes            "    valueRecordDef <"
1962*e1fe3e4aSElliott Hughes            "        1 2 3 4"
1963*e1fe3e4aSElliott Hughes            "        <device 8 88>"
1964*e1fe3e4aSElliott Hughes            "        <device 11 111, 12 112>"
1965*e1fe3e4aSElliott Hughes            "        <device NULL>"
1966*e1fe3e4aSElliott Hughes            "        <device 33 -113, 44 -114, 55 115>"
1967*e1fe3e4aSElliott Hughes            "    > foo;"
1968*e1fe3e4aSElliott Hughes            "} liga;"
1969*e1fe3e4aSElliott Hughes        )
1970*e1fe3e4aSElliott Hughes        value = doc.statements[0].statements[0].value
1971*e1fe3e4aSElliott Hughes        self.assertEqual(value.xPlacement, 1)
1972*e1fe3e4aSElliott Hughes        self.assertEqual(value.yPlacement, 2)
1973*e1fe3e4aSElliott Hughes        self.assertEqual(value.xAdvance, 3)
1974*e1fe3e4aSElliott Hughes        self.assertEqual(value.yAdvance, 4)
1975*e1fe3e4aSElliott Hughes        self.assertEqual(value.xPlaDevice, ((8, 88),))
1976*e1fe3e4aSElliott Hughes        self.assertEqual(value.yPlaDevice, ((11, 111), (12, 112)))
1977*e1fe3e4aSElliott Hughes        self.assertIsNone(value.xAdvDevice)
1978*e1fe3e4aSElliott Hughes        self.assertEqual(value.yAdvDevice, ((33, -113), (44, -114), (55, 115)))
1979*e1fe3e4aSElliott Hughes        self.assertEqual(
1980*e1fe3e4aSElliott Hughes            value.asFea(),
1981*e1fe3e4aSElliott Hughes            "<1 2 3 4 <device 8 88> <device 11 111, 12 112>"
1982*e1fe3e4aSElliott Hughes            " <device NULL> <device 33 -113, 44 -114, 55 115>>",
1983*e1fe3e4aSElliott Hughes        )
1984*e1fe3e4aSElliott Hughes
1985*e1fe3e4aSElliott Hughes    def test_valuerecord_format_d(self):
1986*e1fe3e4aSElliott Hughes        doc = self.parse("feature test {valueRecordDef <NULL> foo;} test;")
1987*e1fe3e4aSElliott Hughes        value = doc.statements[0].statements[0].value
1988*e1fe3e4aSElliott Hughes        self.assertFalse(value)
1989*e1fe3e4aSElliott Hughes        self.assertEqual(value.asFea(), "<NULL>")
1990*e1fe3e4aSElliott Hughes
1991*e1fe3e4aSElliott Hughes    def test_valuerecord_variable_scalar(self):
1992*e1fe3e4aSElliott Hughes        doc = self.parse(
1993*e1fe3e4aSElliott Hughes            "feature test {valueRecordDef <0 (wght=200:-100 wght=900:-150 wdth=150,wght=900:-120) 0 0> foo;} test;"
1994*e1fe3e4aSElliott Hughes        )
1995*e1fe3e4aSElliott Hughes        value = doc.statements[0].statements[0].value
1996*e1fe3e4aSElliott Hughes        self.assertEqual(
1997*e1fe3e4aSElliott Hughes            value.asFea(),
1998*e1fe3e4aSElliott Hughes            "<0 (wght=200:-100 wght=900:-150 wdth=150,wght=900:-120) 0 0>",
1999*e1fe3e4aSElliott Hughes        )
2000*e1fe3e4aSElliott Hughes
2001*e1fe3e4aSElliott Hughes    def test_valuerecord_named(self):
2002*e1fe3e4aSElliott Hughes        doc = self.parse(
2003*e1fe3e4aSElliott Hughes            "valueRecordDef <1 2 3 4> foo;"
2004*e1fe3e4aSElliott Hughes            "feature liga {valueRecordDef <foo> bar;} liga;"
2005*e1fe3e4aSElliott Hughes        )
2006*e1fe3e4aSElliott Hughes        value = doc.statements[1].statements[0].value
2007*e1fe3e4aSElliott Hughes        self.assertEqual(value.xPlacement, 1)
2008*e1fe3e4aSElliott Hughes        self.assertEqual(value.yPlacement, 2)
2009*e1fe3e4aSElliott Hughes        self.assertEqual(value.xAdvance, 3)
2010*e1fe3e4aSElliott Hughes        self.assertEqual(value.yAdvance, 4)
2011*e1fe3e4aSElliott Hughes
2012*e1fe3e4aSElliott Hughes    def test_valuerecord_named_unknown(self):
2013*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
2014*e1fe3e4aSElliott Hughes            FeatureLibError,
2015*e1fe3e4aSElliott Hughes            'Unknown valueRecordDef "unknown"',
2016*e1fe3e4aSElliott Hughes            self.parse,
2017*e1fe3e4aSElliott Hughes            "valueRecordDef <unknown> foo;",
2018*e1fe3e4aSElliott Hughes        )
2019*e1fe3e4aSElliott Hughes
2020*e1fe3e4aSElliott Hughes    def test_valuerecord_scoping(self):
2021*e1fe3e4aSElliott Hughes        [foo, liga, smcp] = self.parse(
2022*e1fe3e4aSElliott Hughes            "valueRecordDef 789 foo;"
2023*e1fe3e4aSElliott Hughes            "feature liga {valueRecordDef <foo> bar;} liga;"
2024*e1fe3e4aSElliott Hughes            "feature smcp {valueRecordDef <foo> bar;} smcp;"
2025*e1fe3e4aSElliott Hughes        ).statements
2026*e1fe3e4aSElliott Hughes        self.assertEqual(foo.value.xAdvance, 789)
2027*e1fe3e4aSElliott Hughes        self.assertEqual(liga.statements[0].value.xAdvance, 789)
2028*e1fe3e4aSElliott Hughes        self.assertEqual(smcp.statements[0].value.xAdvance, 789)
2029*e1fe3e4aSElliott Hughes
2030*e1fe3e4aSElliott Hughes    def test_valuerecord_device_value_out_of_range(self):
2031*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
2032*e1fe3e4aSElliott Hughes            FeatureLibError,
2033*e1fe3e4aSElliott Hughes            r"Device value out of valid range \(-128..127\)",
2034*e1fe3e4aSElliott Hughes            self.parse,
2035*e1fe3e4aSElliott Hughes            "valueRecordDef <1 2 3 4 <device NULL> <device NULL> "
2036*e1fe3e4aSElliott Hughes            "<device NULL> <device 11 128>> foo;",
2037*e1fe3e4aSElliott Hughes        )
2038*e1fe3e4aSElliott Hughes
2039*e1fe3e4aSElliott Hughes    def test_conditionset(self):
2040*e1fe3e4aSElliott Hughes        doc = self.parse("conditionset heavy { wght 700 900; } heavy;")
2041*e1fe3e4aSElliott Hughes        value = doc.statements[0]
2042*e1fe3e4aSElliott Hughes        self.assertEqual(value.conditions["wght"], (700, 900))
2043*e1fe3e4aSElliott Hughes        self.assertEqual(
2044*e1fe3e4aSElliott Hughes            value.asFea(), "conditionset heavy {\n    wght 700 900;\n} heavy;\n"
2045*e1fe3e4aSElliott Hughes        )
2046*e1fe3e4aSElliott Hughes
2047*e1fe3e4aSElliott Hughes        doc = self.parse("conditionset heavy { wght 700 900; opsz 17 18;} heavy;")
2048*e1fe3e4aSElliott Hughes        value = doc.statements[0]
2049*e1fe3e4aSElliott Hughes        self.assertEqual(value.conditions["wght"], (700, 900))
2050*e1fe3e4aSElliott Hughes        self.assertEqual(value.conditions["opsz"], (17, 18))
2051*e1fe3e4aSElliott Hughes        self.assertEqual(
2052*e1fe3e4aSElliott Hughes            value.asFea(),
2053*e1fe3e4aSElliott Hughes            "conditionset heavy {\n    wght 700 900;\n    opsz 17 18;\n} heavy;\n",
2054*e1fe3e4aSElliott Hughes        )
2055*e1fe3e4aSElliott Hughes
2056*e1fe3e4aSElliott Hughes    def test_conditionset_same_axis(self):
2057*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
2058*e1fe3e4aSElliott Hughes            FeatureLibError,
2059*e1fe3e4aSElliott Hughes            r"Repeated condition for axis wght",
2060*e1fe3e4aSElliott Hughes            self.parse,
2061*e1fe3e4aSElliott Hughes            "conditionset heavy { wght 700 900; wght 100 200; } heavy;",
2062*e1fe3e4aSElliott Hughes        )
2063*e1fe3e4aSElliott Hughes
2064*e1fe3e4aSElliott Hughes    def test_conditionset_float(self):
2065*e1fe3e4aSElliott Hughes        doc = self.parse("conditionset heavy { wght 700.0 900.0; } heavy;")
2066*e1fe3e4aSElliott Hughes        value = doc.statements[0]
2067*e1fe3e4aSElliott Hughes        self.assertEqual(value.conditions["wght"], (700.0, 900.0))
2068*e1fe3e4aSElliott Hughes        self.assertEqual(
2069*e1fe3e4aSElliott Hughes            value.asFea(), "conditionset heavy {\n    wght 700.0 900.0;\n} heavy;\n"
2070*e1fe3e4aSElliott Hughes        )
2071*e1fe3e4aSElliott Hughes
2072*e1fe3e4aSElliott Hughes    def test_variation(self):
2073*e1fe3e4aSElliott Hughes        doc = self.parse("variation rvrn heavy { sub a by b; } rvrn;")
2074*e1fe3e4aSElliott Hughes        value = doc.statements[0]
2075*e1fe3e4aSElliott Hughes
2076*e1fe3e4aSElliott Hughes    def test_languagesystem(self):
2077*e1fe3e4aSElliott Hughes        [langsys] = self.parse("languagesystem latn DEU;").statements
2078*e1fe3e4aSElliott Hughes        self.assertEqual(langsys.script, "latn")
2079*e1fe3e4aSElliott Hughes        self.assertEqual(langsys.language, "DEU ")
2080*e1fe3e4aSElliott Hughes        [langsys] = self.parse("languagesystem DFLT DEU;").statements
2081*e1fe3e4aSElliott Hughes        self.assertEqual(langsys.script, "DFLT")
2082*e1fe3e4aSElliott Hughes        self.assertEqual(langsys.language, "DEU ")
2083*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
2084*e1fe3e4aSElliott Hughes            FeatureLibError,
2085*e1fe3e4aSElliott Hughes            '"dflt" is not a valid script tag; use "DFLT" instead',
2086*e1fe3e4aSElliott Hughes            self.parse,
2087*e1fe3e4aSElliott Hughes            "languagesystem dflt dflt;",
2088*e1fe3e4aSElliott Hughes        )
2089*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
2090*e1fe3e4aSElliott Hughes            FeatureLibError,
2091*e1fe3e4aSElliott Hughes            '"DFLT" is not a valid language tag; use "dflt" instead',
2092*e1fe3e4aSElliott Hughes            self.parse,
2093*e1fe3e4aSElliott Hughes            "languagesystem latn DFLT;",
2094*e1fe3e4aSElliott Hughes        )
2095*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
2096*e1fe3e4aSElliott Hughes            FeatureLibError, "Expected ';'", self.parse, "languagesystem latn DEU"
2097*e1fe3e4aSElliott Hughes        )
2098*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
2099*e1fe3e4aSElliott Hughes            FeatureLibError,
2100*e1fe3e4aSElliott Hughes            "longer than 4 characters",
2101*e1fe3e4aSElliott Hughes            self.parse,
2102*e1fe3e4aSElliott Hughes            "languagesystem foobar DEU;",
2103*e1fe3e4aSElliott Hughes        )
2104*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
2105*e1fe3e4aSElliott Hughes            FeatureLibError,
2106*e1fe3e4aSElliott Hughes            "longer than 4 characters",
2107*e1fe3e4aSElliott Hughes            self.parse,
2108*e1fe3e4aSElliott Hughes            "languagesystem latn FOOBAR;",
2109*e1fe3e4aSElliott Hughes        )
2110*e1fe3e4aSElliott Hughes
2111*e1fe3e4aSElliott Hughes    def test_empty_statement_ignored(self):
2112*e1fe3e4aSElliott Hughes        doc = self.parse("feature test {;} test;")
2113*e1fe3e4aSElliott Hughes        self.assertFalse(doc.statements[0].statements)
2114*e1fe3e4aSElliott Hughes        doc = self.parse(";;;")
2115*e1fe3e4aSElliott Hughes        self.assertFalse(doc.statements)
2116*e1fe3e4aSElliott Hughes        for table in "BASE GDEF OS/2 head hhea name vhea".split():
2117*e1fe3e4aSElliott Hughes            doc = self.parse("table %s { ;;; } %s;" % (table, table))
2118*e1fe3e4aSElliott Hughes            self.assertEqual(doc.statements[0].statements, [])
2119*e1fe3e4aSElliott Hughes
2120*e1fe3e4aSElliott Hughes    def test_ufo_features_parse_include_dir(self):
2121*e1fe3e4aSElliott Hughes        fea_path = self.getpath("include/test.ufo/features.fea")
2122*e1fe3e4aSElliott Hughes        include_dir = os.path.dirname(os.path.dirname(fea_path))
2123*e1fe3e4aSElliott Hughes        doc = Parser(fea_path, includeDir=include_dir).parse()
2124*e1fe3e4aSElliott Hughes        assert len(doc.statements) == 1 and doc.statements[0].text == "# Nothing"
2125*e1fe3e4aSElliott Hughes
2126*e1fe3e4aSElliott Hughes    def test_unmarked_ignore_statement(self):
2127*e1fe3e4aSElliott Hughes        with CapturingLogHandler("fontTools.feaLib.parser", level="WARNING") as caplog:
2128*e1fe3e4aSElliott Hughes            doc = self.parse("lookup foo { ignore sub A; } foo;")
2129*e1fe3e4aSElliott Hughes        self.assertEqual(doc.statements[0].statements[0].asFea(), "ignore sub A';")
2130*e1fe3e4aSElliott Hughes        self.assertEqual(len(caplog.records), 1)
2131*e1fe3e4aSElliott Hughes        caplog.assertRegex(
2132*e1fe3e4aSElliott Hughes            'Ambiguous "ignore sub", there should be least one marked glyph'
2133*e1fe3e4aSElliott Hughes        )
2134*e1fe3e4aSElliott Hughes
2135*e1fe3e4aSElliott Hughes    def parse(self, text, glyphNames=GLYPHNAMES, followIncludes=True):
2136*e1fe3e4aSElliott Hughes        featurefile = StringIO(text)
2137*e1fe3e4aSElliott Hughes        p = Parser(featurefile, glyphNames, followIncludes=followIncludes)
2138*e1fe3e4aSElliott Hughes        return p.parse()
2139*e1fe3e4aSElliott Hughes
2140*e1fe3e4aSElliott Hughes    @staticmethod
2141*e1fe3e4aSElliott Hughes    def getpath(testfile):
2142*e1fe3e4aSElliott Hughes        path, _ = os.path.split(__file__)
2143*e1fe3e4aSElliott Hughes        return os.path.join(path, "data", testfile)
2144*e1fe3e4aSElliott Hughes
2145*e1fe3e4aSElliott Hughes
2146*e1fe3e4aSElliott Hughesclass SymbolTableTest(unittest.TestCase):
2147*e1fe3e4aSElliott Hughes    def test_scopes(self):
2148*e1fe3e4aSElliott Hughes        symtab = SymbolTable()
2149*e1fe3e4aSElliott Hughes        symtab.define("foo", 23)
2150*e1fe3e4aSElliott Hughes        self.assertEqual(symtab.resolve("foo"), 23)
2151*e1fe3e4aSElliott Hughes        symtab.enter_scope()
2152*e1fe3e4aSElliott Hughes        self.assertEqual(symtab.resolve("foo"), 23)
2153*e1fe3e4aSElliott Hughes        symtab.define("foo", 42)
2154*e1fe3e4aSElliott Hughes        self.assertEqual(symtab.resolve("foo"), 42)
2155*e1fe3e4aSElliott Hughes        symtab.exit_scope()
2156*e1fe3e4aSElliott Hughes        self.assertEqual(symtab.resolve("foo"), 23)
2157*e1fe3e4aSElliott Hughes
2158*e1fe3e4aSElliott Hughes    def test_resolve_undefined(self):
2159*e1fe3e4aSElliott Hughes        self.assertEqual(SymbolTable().resolve("abc"), None)
2160*e1fe3e4aSElliott Hughes
2161*e1fe3e4aSElliott Hughes
2162*e1fe3e4aSElliott Hughesif __name__ == "__main__":
2163*e1fe3e4aSElliott Hughes    import sys
2164*e1fe3e4aSElliott Hughes
2165*e1fe3e4aSElliott Hughes    sys.exit(unittest.main())
2166