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