xref: /aosp_15_r20/external/fonttools/Tests/voltLib/volttofea_test.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1import pathlib
2import shutil
3import tempfile
4import unittest
5from io import StringIO
6
7from fontTools.voltLib.voltToFea import VoltToFea
8
9DATADIR = pathlib.Path(__file__).parent / "data"
10
11
12class ToFeaTest(unittest.TestCase):
13    @classmethod
14    def setup_class(cls):
15        cls.tempdir = None
16        cls.num_tempfiles = 0
17
18    @classmethod
19    def teardown_class(cls):
20        if cls.tempdir:
21            shutil.rmtree(cls.tempdir, ignore_errors=True)
22
23    @classmethod
24    def temp_path(cls):
25        if not cls.tempdir:
26            cls.tempdir = pathlib.Path(tempfile.mkdtemp())
27        cls.num_tempfiles += 1
28        return cls.tempdir / f"tmp{cls.num_tempfiles}"
29
30    def test_def_glyph_base(self):
31        fea = self.parse('DEF_GLYPH ".notdef" ID 0 TYPE BASE END_GLYPH')
32        self.assertEqual(
33            fea,
34            "@GDEF_base = [.notdef];\n"
35            "table GDEF {\n"
36            "    GlyphClassDef @GDEF_base, , , ;\n"
37            "} GDEF;\n",
38        )
39
40    def test_def_glyph_base_2_components(self):
41        fea = self.parse(
42            'DEF_GLYPH "glyphBase" ID 320 TYPE BASE COMPONENTS 2 END_GLYPH'
43        )
44        self.assertEqual(
45            fea,
46            "@GDEF_base = [glyphBase];\n"
47            "table GDEF {\n"
48            "    GlyphClassDef @GDEF_base, , , ;\n"
49            "} GDEF;\n",
50        )
51
52    def test_def_glyph_ligature_2_components(self):
53        fea = self.parse('DEF_GLYPH "f_f" ID 320 TYPE LIGATURE COMPONENTS 2 END_GLYPH')
54        self.assertEqual(
55            fea,
56            "@GDEF_ligature = [f_f];\n"
57            "table GDEF {\n"
58            "    GlyphClassDef , @GDEF_ligature, , ;\n"
59            "} GDEF;\n",
60        )
61
62    def test_def_glyph_mark(self):
63        fea = self.parse('DEF_GLYPH "brevecomb" ID 320 TYPE MARK END_GLYPH')
64        self.assertEqual(
65            fea,
66            "@GDEF_mark = [brevecomb];\n"
67            "table GDEF {\n"
68            "    GlyphClassDef , , @GDEF_mark, ;\n"
69            "} GDEF;\n",
70        )
71
72    def test_def_glyph_component(self):
73        fea = self.parse('DEF_GLYPH "f.f_f" ID 320 TYPE COMPONENT END_GLYPH')
74        self.assertEqual(
75            fea,
76            "@GDEF_component = [f.f_f];\n"
77            "table GDEF {\n"
78            "    GlyphClassDef , , , @GDEF_component;\n"
79            "} GDEF;\n",
80        )
81
82    def test_def_glyph_no_type(self):
83        fea = self.parse('DEF_GLYPH "glyph20" ID 20 END_GLYPH')
84        self.assertEqual(fea, "")
85
86    def test_def_glyph_case_sensitive(self):
87        fea = self.parse(
88            'DEF_GLYPH "A" ID 3 UNICODE 65 TYPE BASE END_GLYPH\n'
89            'DEF_GLYPH "a" ID 4 UNICODE 97 TYPE BASE END_GLYPH\n'
90        )
91        self.assertEqual(
92            fea,
93            "@GDEF_base = [A a];\n"
94            "table GDEF {\n"
95            "    GlyphClassDef @GDEF_base, , , ;\n"
96            "} GDEF;\n",
97        )
98
99    def test_def_group_glyphs(self):
100        fea = self.parse(
101            'DEF_GROUP "aaccented"\n'
102            'ENUM GLYPH "aacute" GLYPH "abreve" GLYPH "acircumflex" '
103            'GLYPH "adieresis" GLYPH "ae" GLYPH "agrave" GLYPH "amacron" '
104            'GLYPH "aogonek" GLYPH "aring" GLYPH "atilde" END_ENUM\n'
105            "END_GROUP\n"
106        )
107        self.assertEqual(
108            fea,
109            "# Glyph classes\n"
110            "@aaccented = [aacute abreve acircumflex adieresis ae"
111            " agrave amacron aogonek aring atilde];",
112        )
113
114    def test_def_group_groups(self):
115        fea = self.parse(
116            'DEF_GROUP "Group1"\n'
117            'ENUM GLYPH "a" GLYPH "b" GLYPH "c" GLYPH "d" END_ENUM\n'
118            "END_GROUP\n"
119            'DEF_GROUP "Group2"\n'
120            'ENUM GLYPH "e" GLYPH "f" GLYPH "g" GLYPH "h" END_ENUM\n'
121            "END_GROUP\n"
122            'DEF_GROUP "TestGroup"\n'
123            'ENUM GROUP "Group1" GROUP "Group2" END_ENUM\n'
124            "END_GROUP\n"
125        )
126        self.assertEqual(
127            fea,
128            "# Glyph classes\n"
129            "@Group1 = [a b c d];\n"
130            "@Group2 = [e f g h];\n"
131            "@TestGroup = [@Group1 @Group2];",
132        )
133
134    def test_def_group_groups_not_yet_defined(self):
135        fea = self.parse(
136            'DEF_GROUP "Group1"\n'
137            'ENUM GLYPH "a" GLYPH "b" GLYPH "c" GLYPH "d" END_ENUM\n'
138            "END_GROUP\n"
139            'DEF_GROUP "TestGroup1"\n'
140            'ENUM GROUP "Group1" GROUP "Group2" END_ENUM\n'
141            "END_GROUP\n"
142            'DEF_GROUP "TestGroup2"\n'
143            'ENUM GROUP "Group2" END_ENUM\n'
144            "END_GROUP\n"
145            'DEF_GROUP "TestGroup3"\n'
146            'ENUM GROUP "Group2" GROUP "Group1" END_ENUM\n'
147            "END_GROUP\n"
148            'DEF_GROUP "Group2"\n'
149            'ENUM GLYPH "e" GLYPH "f" GLYPH "g" GLYPH "h" END_ENUM\n'
150            "END_GROUP\n"
151        )
152        self.assertEqual(
153            fea,
154            "# Glyph classes\n"
155            "@Group1 = [a b c d];\n"
156            "@Group2 = [e f g h];\n"
157            "@TestGroup1 = [@Group1 @Group2];\n"
158            "@TestGroup2 = [@Group2];\n"
159            "@TestGroup3 = [@Group2 @Group1];",
160        )
161
162    def test_def_group_glyphs_and_group(self):
163        fea = self.parse(
164            'DEF_GROUP "aaccented"\n'
165            'ENUM GLYPH "aacute" GLYPH "abreve" GLYPH "acircumflex" '
166            'GLYPH "adieresis" GLYPH "ae" GLYPH "agrave" GLYPH "amacron" '
167            'GLYPH "aogonek" GLYPH "aring" GLYPH "atilde" END_ENUM\n'
168            "END_GROUP\n"
169            'DEF_GROUP "KERN_lc_a_2ND"\n'
170            'ENUM GLYPH "a" GROUP "aaccented" END_ENUM\n'
171            "END_GROUP"
172        )
173        self.assertEqual(
174            fea,
175            "# Glyph classes\n"
176            "@aaccented = [aacute abreve acircumflex adieresis ae"
177            " agrave amacron aogonek aring atilde];\n"
178            "@KERN_lc_a_2ND = [a @aaccented];",
179        )
180
181    def test_def_group_range(self):
182        fea = self.parse(
183            'DEF_GLYPH "a" ID 163 UNICODE 97 TYPE BASE END_GLYPH\n'
184            'DEF_GLYPH "agrave" ID 194 UNICODE 224 TYPE BASE END_GLYPH\n'
185            'DEF_GLYPH "aacute" ID 195 UNICODE 225 TYPE BASE END_GLYPH\n'
186            'DEF_GLYPH "acircumflex" ID 196 UNICODE 226 TYPE BASE END_GLYPH\n'
187            'DEF_GLYPH "atilde" ID 197 UNICODE 227 TYPE BASE END_GLYPH\n'
188            'DEF_GLYPH "c" ID 165 UNICODE 99 TYPE BASE END_GLYPH\n'
189            'DEF_GLYPH "ccaron" ID 209 UNICODE 269 TYPE BASE END_GLYPH\n'
190            'DEF_GLYPH "ccedilla" ID 210 UNICODE 231 TYPE BASE END_GLYPH\n'
191            'DEF_GLYPH "cdotaccent" ID 210 UNICODE 267 TYPE BASE END_GLYPH\n'
192            'DEF_GROUP "KERN_lc_a_2ND"\n'
193            'ENUM RANGE "a" TO "atilde" GLYPH "b" RANGE "c" TO "cdotaccent" '
194            "END_ENUM\n"
195            "END_GROUP"
196        )
197        self.assertEqual(
198            fea,
199            "# Glyph classes\n"
200            "@KERN_lc_a_2ND = [a - atilde b c - cdotaccent];\n"
201            "@GDEF_base = [a agrave aacute acircumflex atilde c"
202            " ccaron ccedilla cdotaccent];\n"
203            "table GDEF {\n"
204            "    GlyphClassDef @GDEF_base, , , ;\n"
205            "} GDEF;\n",
206        )
207
208    def test_script_without_langsys(self):
209        fea = self.parse('DEF_SCRIPT NAME "Latin" TAG "latn"\n' "END_SCRIPT")
210        self.assertEqual(fea, "")
211
212    def test_langsys_normal(self):
213        fea = self.parse(
214            'DEF_SCRIPT NAME "Latin" TAG "latn"\n'
215            'DEF_LANGSYS NAME "Romanian" TAG "ROM "\n'
216            "END_LANGSYS\n"
217            'DEF_LANGSYS NAME "Moldavian" TAG "MOL "\n'
218            "END_LANGSYS\n"
219            "END_SCRIPT"
220        )
221        self.assertEqual(fea, "")
222
223    def test_langsys_no_script_name(self):
224        fea = self.parse(
225            'DEF_SCRIPT TAG "latn"\n'
226            'DEF_LANGSYS NAME "Default" TAG "dflt"\n'
227            "END_LANGSYS\n"
228            "END_SCRIPT"
229        )
230        self.assertEqual(fea, "")
231
232    def test_langsys_lang_in_separate_scripts(self):
233        fea = self.parse(
234            'DEF_SCRIPT NAME "Default" TAG "DFLT"\n'
235            'DEF_LANGSYS NAME "Default" TAG "dflt"\n'
236            "END_LANGSYS\n"
237            'DEF_LANGSYS NAME "Default" TAG "ROM "\n'
238            "END_LANGSYS\n"
239            "END_SCRIPT\n"
240            'DEF_SCRIPT NAME "Latin" TAG "latn"\n'
241            'DEF_LANGSYS NAME "Default" TAG "dflt"\n'
242            "END_LANGSYS\n"
243            'DEF_LANGSYS NAME "Default" TAG "ROM "\n'
244            "END_LANGSYS\n"
245            "END_SCRIPT"
246        )
247        self.assertEqual(fea, "")
248
249    def test_langsys_no_lang_name(self):
250        fea = self.parse(
251            'DEF_SCRIPT NAME "Latin" TAG "latn"\n'
252            'DEF_LANGSYS TAG "dflt"\n'
253            "END_LANGSYS\n"
254            "END_SCRIPT"
255        )
256        self.assertEqual(fea, "")
257
258    def test_feature(self):
259        fea = self.parse(
260            'DEF_SCRIPT NAME "Latin" TAG "latn"\n'
261            'DEF_LANGSYS NAME "Romanian" TAG "ROM "\n'
262            'DEF_FEATURE NAME "Fractions" TAG "frac"\n'
263            'LOOKUP "fraclookup"\n'
264            "END_FEATURE\n"
265            "END_LANGSYS\n"
266            "END_SCRIPT\n"
267            'DEF_LOOKUP "fraclookup" PROCESS_BASE PROCESS_MARKS ALL '
268            "DIRECTION LTR\n"
269            "IN_CONTEXT\n"
270            "END_CONTEXT\n"
271            "AS_SUBSTITUTION\n"
272            'SUB GLYPH "one" GLYPH "slash" GLYPH "two"\n'
273            'WITH GLYPH "one_slash_two.frac"\n'
274            "END_SUB\n"
275            "END_SUBSTITUTION"
276        )
277        self.assertEqual(
278            fea,
279            "\n# Lookups\n"
280            "lookup fraclookup {\n"
281            "    sub one slash two by one_slash_two.frac;\n"
282            "} fraclookup;\n"
283            "\n"
284            "# Features\n"
285            "feature frac {\n"
286            "    script latn;\n"
287            "    language ROM exclude_dflt;\n"
288            "    lookup fraclookup;\n"
289            "} frac;\n",
290        )
291
292    def test_feature_sub_lookups(self):
293        fea = self.parse(
294            'DEF_SCRIPT NAME "Latin" TAG "latn"\n'
295            'DEF_LANGSYS NAME "Romanian" TAG "ROM "\n'
296            'DEF_FEATURE NAME "Fractions" TAG "frac"\n'
297            'LOOKUP "fraclookup\\1"\n'
298            'LOOKUP "fraclookup\\1"\n'
299            "END_FEATURE\n"
300            "END_LANGSYS\n"
301            "END_SCRIPT\n"
302            'DEF_LOOKUP "fraclookup\\1" PROCESS_BASE PROCESS_MARKS ALL '
303            "DIRECTION RTL\n"
304            "IN_CONTEXT\n"
305            "END_CONTEXT\n"
306            "AS_SUBSTITUTION\n"
307            'SUB GLYPH "one" GLYPH "slash" GLYPH "two"\n'
308            'WITH GLYPH "one_slash_two.frac"\n'
309            "END_SUB\n"
310            "END_SUBSTITUTION\n"
311            'DEF_LOOKUP "fraclookup\\2" PROCESS_BASE PROCESS_MARKS ALL '
312            "DIRECTION RTL\n"
313            "IN_CONTEXT\n"
314            "END_CONTEXT\n"
315            "AS_SUBSTITUTION\n"
316            'SUB GLYPH "one" GLYPH "slash" GLYPH "three"\n'
317            'WITH GLYPH "one_slash_three.frac"\n'
318            "END_SUB\n"
319            "END_SUBSTITUTION"
320        )
321        self.assertEqual(
322            fea,
323            "\n# Lookups\n"
324            "lookup fraclookup {\n"
325            "    lookupflag RightToLeft;\n"
326            "    # fraclookup\\1\n"
327            "    sub one slash two by one_slash_two.frac;\n"
328            "    subtable;\n"
329            "    # fraclookup\\2\n"
330            "    sub one slash three by one_slash_three.frac;\n"
331            "} fraclookup;\n"
332            "\n"
333            "# Features\n"
334            "feature frac {\n"
335            "    script latn;\n"
336            "    language ROM exclude_dflt;\n"
337            "    lookup fraclookup;\n"
338            "} frac;\n",
339        )
340
341    def test_lookup_comment(self):
342        fea = self.parse(
343            'DEF_LOOKUP "smcp" PROCESS_BASE PROCESS_MARKS ALL '
344            "DIRECTION LTR\n"
345            'COMMENTS "Smallcaps lookup for testing"\n'
346            "IN_CONTEXT\n"
347            "END_CONTEXT\n"
348            "AS_SUBSTITUTION\n"
349            'SUB GLYPH "a"\n'
350            'WITH GLYPH "a.sc"\n'
351            "END_SUB\n"
352            'SUB GLYPH "b"\n'
353            'WITH GLYPH "b.sc"\n'
354            "END_SUB\n"
355            "END_SUBSTITUTION"
356        )
357        self.assertEqual(
358            fea,
359            "\n# Lookups\n"
360            "lookup smcp {\n"
361            "    # Smallcaps lookup for testing\n"
362            "    sub a by a.sc;\n"
363            "    sub b by b.sc;\n"
364            "} smcp;\n",
365        )
366
367    def test_substitution_single(self):
368        fea = self.parse(
369            'DEF_LOOKUP "smcp" PROCESS_BASE PROCESS_MARKS ALL '
370            "DIRECTION LTR\n"
371            "IN_CONTEXT\n"
372            "END_CONTEXT\n"
373            "AS_SUBSTITUTION\n"
374            'SUB GLYPH "a"\n'
375            'WITH GLYPH "a.sc"\n'
376            "END_SUB\n"
377            'SUB GLYPH "b"\n'
378            'WITH GLYPH "b.sc"\n'
379            "END_SUB\n"
380            "SUB WITH\n"  # Empty substitution, will be ignored
381            "END_SUB\n"
382            "END_SUBSTITUTION"
383        )
384        self.assertEqual(
385            fea,
386            "\n# Lookups\n"
387            "lookup smcp {\n"
388            "    sub a by a.sc;\n"
389            "    sub b by b.sc;\n"
390            "} smcp;\n",
391        )
392
393    def test_substitution_single_in_context(self):
394        fea = self.parse(
395            'DEF_GROUP "Denominators" ENUM GLYPH "one.dnom" GLYPH "two.dnom" '
396            "END_ENUM END_GROUP\n"
397            'DEF_LOOKUP "fracdnom" PROCESS_BASE PROCESS_MARKS ALL '
398            "DIRECTION LTR\n"
399            'IN_CONTEXT LEFT ENUM GROUP "Denominators" GLYPH "fraction" '
400            "END_ENUM\n"
401            "END_CONTEXT\n"
402            "AS_SUBSTITUTION\n"
403            'SUB GLYPH "one"\n'
404            'WITH GLYPH "one.dnom"\n'
405            "END_SUB\n"
406            'SUB GLYPH "two"\n'
407            'WITH GLYPH "two.dnom"\n'
408            "END_SUB\n"
409            "END_SUBSTITUTION"
410        )
411        self.assertEqual(
412            fea,
413            "# Glyph classes\n"
414            "@Denominators = [one.dnom two.dnom];\n"
415            "\n"
416            "# Lookups\n"
417            "lookup fracdnom {\n"
418            "    sub [@Denominators fraction] one' by one.dnom;\n"
419            "    sub [@Denominators fraction] two' by two.dnom;\n"
420            "} fracdnom;\n",
421        )
422
423    def test_substitution_single_in_contexts(self):
424        fea = self.parse(
425            'DEF_GROUP "Hebrew" ENUM GLYPH "uni05D0" GLYPH "uni05D1" '
426            "END_ENUM END_GROUP\n"
427            'DEF_LOOKUP "HebrewCurrency" PROCESS_BASE PROCESS_MARKS ALL '
428            "DIRECTION LTR\n"
429            "IN_CONTEXT\n"
430            'RIGHT GROUP "Hebrew"\n'
431            'RIGHT GLYPH "one.Hebr"\n'
432            "END_CONTEXT\n"
433            "IN_CONTEXT\n"
434            'LEFT GROUP "Hebrew"\n'
435            'LEFT GLYPH "one.Hebr"\n'
436            "END_CONTEXT\n"
437            "AS_SUBSTITUTION\n"
438            'SUB GLYPH "dollar"\n'
439            'WITH GLYPH "dollar.Hebr"\n'
440            "END_SUB\n"
441            "END_SUBSTITUTION"
442        )
443        self.assertEqual(
444            fea,
445            "# Glyph classes\n"
446            "@Hebrew = [uni05D0 uni05D1];\n"
447            "\n"
448            "# Lookups\n"
449            "lookup HebrewCurrency {\n"
450            "    sub dollar' @Hebrew one.Hebr by dollar.Hebr;\n"
451            "    sub @Hebrew one.Hebr dollar' by dollar.Hebr;\n"
452            "} HebrewCurrency;\n",
453        )
454
455    def test_substitution_single_except_context(self):
456        fea = self.parse(
457            'DEF_GROUP "Hebrew" ENUM GLYPH "uni05D0" GLYPH "uni05D1" '
458            "END_ENUM END_GROUP\n"
459            'DEF_LOOKUP "HebrewCurrency" PROCESS_BASE PROCESS_MARKS ALL '
460            "DIRECTION LTR\n"
461            "EXCEPT_CONTEXT\n"
462            'RIGHT GROUP "Hebrew"\n'
463            'RIGHT GLYPH "one.Hebr"\n'
464            "END_CONTEXT\n"
465            "IN_CONTEXT\n"
466            'LEFT GROUP "Hebrew"\n'
467            'LEFT GLYPH "one.Hebr"\n'
468            "END_CONTEXT\n"
469            "AS_SUBSTITUTION\n"
470            'SUB GLYPH "dollar"\n'
471            'WITH GLYPH "dollar.Hebr"\n'
472            "END_SUB\n"
473            "END_SUBSTITUTION"
474        )
475        self.assertEqual(
476            fea,
477            "# Glyph classes\n"
478            "@Hebrew = [uni05D0 uni05D1];\n"
479            "\n"
480            "# Lookups\n"
481            "lookup HebrewCurrency {\n"
482            "    ignore sub dollar' @Hebrew one.Hebr;\n"
483            "    sub @Hebrew one.Hebr dollar' by dollar.Hebr;\n"
484            "} HebrewCurrency;\n",
485        )
486
487    def test_substitution_skip_base(self):
488        fea = self.parse(
489            'DEF_GROUP "SomeMarks" ENUM GLYPH "marka" GLYPH "markb" '
490            "END_ENUM END_GROUP\n"
491            'DEF_LOOKUP "SomeSub" SKIP_BASE PROCESS_MARKS ALL '
492            "DIRECTION LTR\n"
493            "IN_CONTEXT\n"
494            "END_CONTEXT\n"
495            "AS_SUBSTITUTION\n"
496            'SUB GLYPH "A"\n'
497            'WITH GLYPH "A.c2sc"\n'
498            "END_SUB\n"
499            "END_SUBSTITUTION"
500        )
501        self.assertEqual(
502            fea,
503            "# Glyph classes\n"
504            "@SomeMarks = [marka markb];\n"
505            "\n"
506            "# Lookups\n"
507            "lookup SomeSub {\n"
508            "    lookupflag IgnoreBaseGlyphs;\n"
509            "    sub A by A.c2sc;\n"
510            "} SomeSub;\n",
511        )
512
513    def test_substitution_process_base(self):
514        fea = self.parse(
515            'DEF_GROUP "SomeMarks" ENUM GLYPH "marka" GLYPH "markb" '
516            "END_ENUM END_GROUP\n"
517            'DEF_LOOKUP "SomeSub" PROCESS_BASE PROCESS_MARKS ALL '
518            "DIRECTION LTR\n"
519            "IN_CONTEXT\n"
520            "END_CONTEXT\n"
521            "AS_SUBSTITUTION\n"
522            'SUB GLYPH "A"\n'
523            'WITH GLYPH "A.c2sc"\n'
524            "END_SUB\n"
525            "END_SUBSTITUTION"
526        )
527        self.assertEqual(
528            fea,
529            "# Glyph classes\n"
530            "@SomeMarks = [marka markb];\n"
531            "\n"
532            "# Lookups\n"
533            "lookup SomeSub {\n"
534            "    sub A by A.c2sc;\n"
535            "} SomeSub;\n",
536        )
537
538    def test_substitution_process_marks_all(self):
539        fea = self.parse(
540            'DEF_GROUP "SomeMarks" ENUM GLYPH "marka" GLYPH "markb" '
541            "END_ENUM END_GROUP\n"
542            'DEF_LOOKUP "SomeSub" PROCESS_BASE PROCESS_MARKS "ALL"'
543            "DIRECTION LTR\n"
544            "IN_CONTEXT\n"
545            "END_CONTEXT\n"
546            "AS_SUBSTITUTION\n"
547            'SUB GLYPH "A"\n'
548            'WITH GLYPH "A.c2sc"\n'
549            "END_SUB\n"
550            "END_SUBSTITUTION"
551        )
552        self.assertEqual(
553            fea,
554            "# Glyph classes\n"
555            "@SomeMarks = [marka markb];\n"
556            "\n"
557            "# Lookups\n"
558            "lookup SomeSub {\n"
559            "    sub A by A.c2sc;\n"
560            "} SomeSub;\n",
561        )
562
563    def test_substitution_process_marks_none(self):
564        fea = self.parse(
565            'DEF_GROUP "SomeMarks" ENUM GLYPH "marka" GLYPH "markb" '
566            "END_ENUM END_GROUP\n"
567            'DEF_LOOKUP "SomeSub" PROCESS_BASE PROCESS_MARKS "NONE"'
568            "DIRECTION LTR\n"
569            "IN_CONTEXT\n"
570            "END_CONTEXT\n"
571            "AS_SUBSTITUTION\n"
572            'SUB GLYPH "A"\n'
573            'WITH GLYPH "A.c2sc"\n'
574            "END_SUB\n"
575            "END_SUBSTITUTION"
576        )
577        self.assertEqual(
578            fea,
579            "# Glyph classes\n"
580            "@SomeMarks = [marka markb];\n"
581            "\n"
582            "# Lookups\n"
583            "lookup SomeSub {\n"
584            "    lookupflag IgnoreMarks;\n"
585            "    sub A by A.c2sc;\n"
586            "} SomeSub;\n",
587        )
588
589    def test_substitution_skip_marks(self):
590        fea = self.parse(
591            'DEF_GROUP "SomeMarks" ENUM GLYPH "marka" GLYPH "markb" '
592            "END_ENUM END_GROUP\n"
593            'DEF_LOOKUP "SomeSub" PROCESS_BASE SKIP_MARKS '
594            "DIRECTION LTR\n"
595            "IN_CONTEXT\n"
596            "END_CONTEXT\n"
597            "AS_SUBSTITUTION\n"
598            'SUB GLYPH "A"\n'
599            'WITH GLYPH "A.c2sc"\n'
600            "END_SUB\n"
601            "END_SUBSTITUTION"
602        )
603        self.assertEqual(
604            fea,
605            "# Glyph classes\n"
606            "@SomeMarks = [marka markb];\n"
607            "\n"
608            "# Lookups\n"
609            "lookup SomeSub {\n"
610            "    lookupflag IgnoreMarks;\n"
611            "    sub A by A.c2sc;\n"
612            "} SomeSub;\n",
613        )
614
615    def test_substitution_mark_attachment(self):
616        fea = self.parse(
617            'DEF_GROUP "SomeMarks" ENUM GLYPH "acutecmb" GLYPH "gravecmb" '
618            "END_ENUM END_GROUP\n"
619            'DEF_LOOKUP "SomeSub" PROCESS_BASE '
620            'PROCESS_MARKS "SomeMarks" \n'
621            "DIRECTION RTL\n"
622            "AS_SUBSTITUTION\n"
623            'SUB GLYPH "A"\n'
624            'WITH GLYPH "A.c2sc"\n'
625            "END_SUB\n"
626            "END_SUBSTITUTION"
627        )
628        self.assertEqual(
629            fea,
630            "# Glyph classes\n"
631            "@SomeMarks = [acutecmb gravecmb];\n"
632            "\n"
633            "# Lookups\n"
634            "lookup SomeSub {\n"
635            "    lookupflag RightToLeft MarkAttachmentType"
636            " @SomeMarks;\n"
637            "    sub A by A.c2sc;\n"
638            "} SomeSub;\n",
639        )
640
641    def test_substitution_mark_glyph_set(self):
642        fea = self.parse(
643            'DEF_GROUP "SomeMarks" ENUM GLYPH "acutecmb" GLYPH "gravecmb" '
644            "END_ENUM END_GROUP\n"
645            'DEF_LOOKUP "SomeSub" PROCESS_BASE '
646            'PROCESS_MARKS MARK_GLYPH_SET "SomeMarks" \n'
647            "DIRECTION RTL\n"
648            "AS_SUBSTITUTION\n"
649            'SUB GLYPH "A"\n'
650            'WITH GLYPH "A.c2sc"\n'
651            "END_SUB\n"
652            "END_SUBSTITUTION"
653        )
654        self.assertEqual(
655            fea,
656            "# Glyph classes\n"
657            "@SomeMarks = [acutecmb gravecmb];\n"
658            "\n"
659            "# Lookups\n"
660            "lookup SomeSub {\n"
661            "    lookupflag RightToLeft UseMarkFilteringSet"
662            " @SomeMarks;\n"
663            "    sub A by A.c2sc;\n"
664            "} SomeSub;\n",
665        )
666
667    def test_substitution_process_all_marks(self):
668        fea = self.parse(
669            'DEF_GROUP "SomeMarks" ENUM GLYPH "acutecmb" GLYPH "gravecmb" '
670            "END_ENUM END_GROUP\n"
671            'DEF_LOOKUP "SomeSub" PROCESS_BASE '
672            "PROCESS_MARKS ALL \n"
673            "DIRECTION RTL\n"
674            "AS_SUBSTITUTION\n"
675            'SUB GLYPH "A"\n'
676            'WITH GLYPH "A.c2sc"\n'
677            "END_SUB\n"
678            "END_SUBSTITUTION"
679        )
680        self.assertEqual(
681            fea,
682            "# Glyph classes\n"
683            "@SomeMarks = [acutecmb gravecmb];\n"
684            "\n"
685            "# Lookups\n"
686            "lookup SomeSub {\n"
687            "    lookupflag RightToLeft;\n"
688            "    sub A by A.c2sc;\n"
689            "} SomeSub;\n",
690        )
691
692    def test_substitution_no_reversal(self):
693        # TODO: check right context with no reversal
694        fea = self.parse(
695            'DEF_LOOKUP "Lookup" PROCESS_BASE PROCESS_MARKS ALL '
696            "DIRECTION LTR\n"
697            "IN_CONTEXT\n"
698            'RIGHT ENUM GLYPH "a" GLYPH "b" END_ENUM\n'
699            "END_CONTEXT\n"
700            "AS_SUBSTITUTION\n"
701            'SUB GLYPH "a"\n'
702            'WITH GLYPH "a.alt"\n'
703            "END_SUB\n"
704            "END_SUBSTITUTION"
705        )
706        self.assertEqual(
707            fea,
708            "\n# Lookups\n"
709            "lookup Lookup {\n"
710            "    sub a' [a b] by a.alt;\n"
711            "} Lookup;\n",
712        )
713
714    def test_substitution_reversal(self):
715        fea = self.parse(
716            'DEF_GROUP "DFLT_Num_standardFigures"\n'
717            'ENUM GLYPH "zero" GLYPH "one" GLYPH "two" END_ENUM\n'
718            "END_GROUP\n"
719            'DEF_GROUP "DFLT_Num_numerators"\n'
720            'ENUM GLYPH "zero.numr" GLYPH "one.numr" GLYPH "two.numr" END_ENUM\n'
721            "END_GROUP\n"
722            'DEF_LOOKUP "RevLookup" PROCESS_BASE PROCESS_MARKS ALL '
723            "DIRECTION LTR REVERSAL\n"
724            "IN_CONTEXT\n"
725            'RIGHT ENUM GLYPH "a" GLYPH "b" END_ENUM\n'
726            "END_CONTEXT\n"
727            "AS_SUBSTITUTION\n"
728            'SUB GROUP "DFLT_Num_standardFigures"\n'
729            'WITH GROUP "DFLT_Num_numerators"\n'
730            "END_SUB\n"
731            "END_SUBSTITUTION"
732        )
733        self.assertEqual(
734            fea,
735            "# Glyph classes\n"
736            "@DFLT_Num_standardFigures = [zero one two];\n"
737            "@DFLT_Num_numerators = [zero.numr one.numr two.numr];\n"
738            "\n"
739            "# Lookups\n"
740            "lookup RevLookup {\n"
741            "    rsub @DFLT_Num_standardFigures' [a b] by @DFLT_Num_numerators;\n"
742            "} RevLookup;\n",
743        )
744
745    def test_substitution_single_to_multiple(self):
746        fea = self.parse(
747            'DEF_LOOKUP "ccmp" PROCESS_BASE PROCESS_MARKS ALL '
748            "DIRECTION LTR\n"
749            "IN_CONTEXT\n"
750            "END_CONTEXT\n"
751            "AS_SUBSTITUTION\n"
752            'SUB GLYPH "aacute"\n'
753            'WITH GLYPH "a" GLYPH "acutecomb"\n'
754            "END_SUB\n"
755            'SUB GLYPH "agrave"\n'
756            'WITH GLYPH "a" GLYPH "gravecomb"\n'
757            "END_SUB\n"
758            "END_SUBSTITUTION"
759        )
760        self.assertEqual(
761            fea,
762            "\n# Lookups\n"
763            "lookup ccmp {\n"
764            "    sub aacute by a acutecomb;\n"
765            "    sub agrave by a gravecomb;\n"
766            "} ccmp;\n",
767        )
768
769    def test_substitution_multiple_to_single(self):
770        fea = self.parse(
771            'DEF_LOOKUP "liga" PROCESS_BASE PROCESS_MARKS ALL '
772            "DIRECTION LTR\n"
773            "IN_CONTEXT\n"
774            "END_CONTEXT\n"
775            "AS_SUBSTITUTION\n"
776            'SUB GLYPH "f" GLYPH "i"\n'
777            'WITH GLYPH "f_i"\n'
778            "END_SUB\n"
779            'SUB GLYPH "f" GLYPH "t"\n'
780            'WITH GLYPH "f_t"\n'
781            "END_SUB\n"
782            "END_SUBSTITUTION"
783        )
784        self.assertEqual(
785            fea,
786            "\n# Lookups\n"
787            "lookup liga {\n"
788            "    sub f i by f_i;\n"
789            "    sub f t by f_t;\n"
790            "} liga;\n",
791        )
792
793    def test_substitution_reverse_chaining_single(self):
794        fea = self.parse(
795            'DEF_LOOKUP "numr" PROCESS_BASE PROCESS_MARKS ALL '
796            "DIRECTION LTR REVERSAL\n"
797            "IN_CONTEXT\n"
798            "RIGHT ENUM "
799            'GLYPH "fraction" '
800            'RANGE "zero.numr" TO "nine.numr" '
801            "END_ENUM\n"
802            "END_CONTEXT\n"
803            "AS_SUBSTITUTION\n"
804            'SUB RANGE "zero" TO "nine"\n'
805            'WITH RANGE "zero.numr" TO "nine.numr"\n'
806            "END_SUB\n"
807            "END_SUBSTITUTION"
808        )
809        self.assertEqual(
810            fea,
811            "\n# Lookups\n"
812            "lookup numr {\n"
813            "    rsub zero - nine' [fraction zero.numr - nine.numr] by zero.numr - nine.numr;\n"
814            "} numr;\n",
815        )
816
817    # GPOS
818    #  ATTACH_CURSIVE
819    #  ATTACH
820    #  ADJUST_PAIR
821    #  ADJUST_SINGLE
822    def test_position_attach(self):
823        fea = self.parse(
824            'DEF_LOOKUP "anchor_top" PROCESS_BASE PROCESS_MARKS ALL '
825            "DIRECTION RTL\n"
826            "IN_CONTEXT\n"
827            "END_CONTEXT\n"
828            "AS_POSITION\n"
829            'ATTACH GLYPH "a" GLYPH "e"\n'
830            'TO GLYPH "acutecomb" AT ANCHOR "top" '
831            'GLYPH "gravecomb" AT ANCHOR "top"\n'
832            "END_ATTACH\n"
833            "END_POSITION\n"
834            'DEF_ANCHOR "MARK_top" ON 120 GLYPH acutecomb COMPONENT 1 '
835            "AT POS DX 0 DY 450 END_POS END_ANCHOR\n"
836            'DEF_ANCHOR "MARK_top" ON 121 GLYPH gravecomb COMPONENT 1 '
837            "AT POS DX 0 DY 450 END_POS END_ANCHOR\n"
838            'DEF_ANCHOR "top" ON 31 GLYPH a COMPONENT 1 '
839            "AT POS DX 210 DY 450 END_POS END_ANCHOR\n"
840            'DEF_ANCHOR "top" ON 35 GLYPH e COMPONENT 1 '
841            "AT POS DX 215 DY 450 END_POS END_ANCHOR\n"
842        )
843        self.assertEqual(
844            fea,
845            "\n# Mark classes\n"
846            "markClass acutecomb <anchor 0 450> @top;\n"
847            "markClass gravecomb <anchor 0 450> @top;\n"
848            "\n"
849            "# Lookups\n"
850            "lookup anchor_top {\n"
851            "    lookupflag RightToLeft;\n"
852            "    pos base a\n"
853            "        <anchor 210 450> mark @top;\n"
854            "    pos base e\n"
855            "        <anchor 215 450> mark @top;\n"
856            "} anchor_top;\n",
857        )
858
859    def test_position_attach_mkmk(self):
860        fea = self.parse(
861            'DEF_GLYPH "brevecomb" ID 1 TYPE MARK END_GLYPH\n'
862            'DEF_GLYPH "gravecomb" ID 2 TYPE MARK END_GLYPH\n'
863            'DEF_LOOKUP "anchor_top" PROCESS_BASE PROCESS_MARKS ALL '
864            "DIRECTION RTL\n"
865            "IN_CONTEXT\n"
866            "END_CONTEXT\n"
867            "AS_POSITION\n"
868            'ATTACH GLYPH "gravecomb"\n'
869            'TO GLYPH "acutecomb" AT ANCHOR "top"\n'
870            "END_ATTACH\n"
871            "END_POSITION\n"
872            'DEF_ANCHOR "MARK_top" ON 1 GLYPH acutecomb COMPONENT 1 '
873            "AT POS DX 0 DY 450 END_POS END_ANCHOR\n"
874            'DEF_ANCHOR "top" ON 2 GLYPH gravecomb COMPONENT 1 '
875            "AT POS DX 210 DY 450 END_POS END_ANCHOR\n"
876        )
877        self.assertEqual(
878            fea,
879            "\n# Mark classes\n"
880            "markClass acutecomb <anchor 0 450> @top;\n"
881            "\n"
882            "# Lookups\n"
883            "lookup anchor_top {\n"
884            "    lookupflag RightToLeft;\n"
885            "    pos mark gravecomb\n"
886            "        <anchor 210 450> mark @top;\n"
887            "} anchor_top;\n"
888            "\n"
889            "@GDEF_mark = [brevecomb gravecomb];\n"
890            "table GDEF {\n"
891            "    GlyphClassDef , , @GDEF_mark, ;\n"
892            "} GDEF;\n",
893        )
894
895    def test_position_attach_in_context(self):
896        fea = self.parse(
897            'DEF_LOOKUP "test" PROCESS_BASE PROCESS_MARKS ALL '
898            "DIRECTION RTL\n"
899            'EXCEPT_CONTEXT LEFT GLYPH "a" END_CONTEXT\n'
900            "AS_POSITION\n"
901            'ATTACH GLYPH "a"\n'
902            'TO GLYPH "acutecomb" AT ANCHOR "top" '
903            'GLYPH "gravecomb" AT ANCHOR "top"\n'
904            "END_ATTACH\n"
905            "END_POSITION\n"
906            'DEF_ANCHOR "MARK_top" ON 120 GLYPH acutecomb COMPONENT 1 '
907            "AT POS DX 0 DY 450 END_POS END_ANCHOR\n"
908            'DEF_ANCHOR "MARK_top" ON 121 GLYPH gravecomb COMPONENT 1 '
909            "AT POS DX 0 DY 450 END_POS END_ANCHOR\n"
910            'DEF_ANCHOR "top" ON 31 GLYPH a COMPONENT 1 '
911            "AT POS DX 210 DY 450 END_POS END_ANCHOR\n"
912        )
913        self.assertEqual(
914            fea,
915            "\n# Mark classes\n"
916            "markClass acutecomb <anchor 0 450> @top;\n"
917            "markClass gravecomb <anchor 0 450> @top;\n"
918            "\n"
919            "# Lookups\n"
920            "lookup test_target {\n"
921            "    pos base a\n"
922            "        <anchor 210 450> mark @top;\n"
923            "} test_target;\n"
924            "\n"
925            "lookup test {\n"
926            "    lookupflag RightToLeft;\n"
927            "    ignore pos a [acutecomb gravecomb]';\n"
928            "    pos [acutecomb gravecomb]' lookup test_target;\n"
929            "} test;\n",
930        )
931
932    def test_position_attach_cursive(self):
933        fea = self.parse(
934            'DEF_LOOKUP "SomeLookup" PROCESS_BASE PROCESS_MARKS ALL '
935            "DIRECTION RTL\n"
936            "IN_CONTEXT\n"
937            "END_CONTEXT\n"
938            "AS_POSITION\n"
939            'ATTACH_CURSIVE EXIT GLYPH "a" GLYPH "b" '
940            'ENTER GLYPH "a" GLYPH "c"\n'
941            "END_ATTACH\n"
942            "END_POSITION\n"
943            'DEF_ANCHOR "exit"  ON 1 GLYPH a COMPONENT 1 AT POS END_POS END_ANCHOR\n'
944            'DEF_ANCHOR "entry" ON 1 GLYPH a COMPONENT 1 AT POS END_POS END_ANCHOR\n'
945            'DEF_ANCHOR "exit"  ON 2 GLYPH b COMPONENT 1 AT POS END_POS END_ANCHOR\n'
946            'DEF_ANCHOR "entry" ON 3 GLYPH c COMPONENT 1 AT POS END_POS END_ANCHOR\n'
947        )
948        self.assertEqual(
949            fea,
950            "\n# Lookups\n"
951            "lookup SomeLookup {\n"
952            "    lookupflag RightToLeft;\n"
953            "    pos cursive a <anchor 0 0> <anchor 0 0>;\n"
954            "    pos cursive c <anchor 0 0> <anchor NULL>;\n"
955            "    pos cursive b <anchor NULL> <anchor 0 0>;\n"
956            "} SomeLookup;\n",
957        )
958
959    def test_position_adjust_pair(self):
960        fea = self.parse(
961            'DEF_LOOKUP "kern1" PROCESS_BASE PROCESS_MARKS ALL '
962            "DIRECTION RTL\n"
963            "IN_CONTEXT\n"
964            "END_CONTEXT\n"
965            "AS_POSITION\n"
966            "ADJUST_PAIR\n"
967            ' FIRST GLYPH "A" FIRST GLYPH "V"\n'
968            ' SECOND GLYPH "A" SECOND GLYPH "V"\n'
969            " 1 2 BY POS ADV -30 END_POS POS END_POS\n"
970            " 2 1 BY POS ADV -25 END_POS POS END_POS\n"
971            "END_ADJUST\n"
972            "END_POSITION\n"
973        )
974        self.assertEqual(
975            fea,
976            "\n# Lookups\n"
977            "lookup kern1 {\n"
978            "    lookupflag RightToLeft;\n"
979            "    enum pos A V -30;\n"
980            "    enum pos V A -25;\n"
981            "} kern1;\n",
982        )
983
984    def test_position_adjust_pair_in_context(self):
985        fea = self.parse(
986            'DEF_LOOKUP "kern1" PROCESS_BASE PROCESS_MARKS ALL '
987            "DIRECTION LTR\n"
988            'EXCEPT_CONTEXT LEFT GLYPH "A" END_CONTEXT\n'
989            "AS_POSITION\n"
990            "ADJUST_PAIR\n"
991            ' FIRST GLYPH "A" FIRST GLYPH "V"\n'
992            ' SECOND GLYPH "A" SECOND GLYPH "V"\n'
993            " 2 1 BY POS ADV -25 END_POS POS END_POS\n"
994            "END_ADJUST\n"
995            "END_POSITION\n"
996        )
997        self.assertEqual(
998            fea,
999            "\n# Lookups\n"
1000            "lookup kern1_target {\n"
1001            "    enum pos V A -25;\n"
1002            "} kern1_target;\n"
1003            "\n"
1004            "lookup kern1 {\n"
1005            "    ignore pos A V' A';\n"
1006            "    pos V' lookup kern1_target A' lookup kern1_target;\n"
1007            "} kern1;\n",
1008        )
1009
1010    def test_position_adjust_single(self):
1011        fea = self.parse(
1012            'DEF_LOOKUP "TestLookup" PROCESS_BASE PROCESS_MARKS ALL '
1013            "DIRECTION LTR\n"
1014            "IN_CONTEXT\n"
1015            "END_CONTEXT\n"
1016            "AS_POSITION\n"
1017            "ADJUST_SINGLE"
1018            ' GLYPH "glyph1" BY POS ADV 0 DX 123 END_POS\n'
1019            ' GLYPH "glyph2" BY POS ADV 0 DX 456 END_POS\n'
1020            "END_ADJUST\n"
1021            "END_POSITION\n"
1022        )
1023        self.assertEqual(
1024            fea,
1025            "\n# Lookups\n"
1026            "lookup TestLookup {\n"
1027            "    pos glyph1 <123 0 0 0>;\n"
1028            "    pos glyph2 <456 0 0 0>;\n"
1029            "} TestLookup;\n",
1030        )
1031
1032    def test_position_adjust_single_in_context(self):
1033        fea = self.parse(
1034            'DEF_LOOKUP "TestLookup" PROCESS_BASE PROCESS_MARKS ALL '
1035            "DIRECTION LTR\n"
1036            "EXCEPT_CONTEXT\n"
1037            'LEFT GLYPH "leftGlyph"\n'
1038            'RIGHT GLYPH "rightGlyph"\n'
1039            "END_CONTEXT\n"
1040            "AS_POSITION\n"
1041            "ADJUST_SINGLE"
1042            ' GLYPH "glyph1" BY POS ADV 0 DX 123 END_POS\n'
1043            ' GLYPH "glyph2" BY POS ADV 0 DX 456 END_POS\n'
1044            "END_ADJUST\n"
1045            "END_POSITION\n"
1046        )
1047        self.assertEqual(
1048            fea,
1049            "\n# Lookups\n"
1050            "lookup TestLookup_target {\n"
1051            "    pos glyph1 <123 0 0 0>;\n"
1052            "    pos glyph2 <456 0 0 0>;\n"
1053            "} TestLookup_target;\n"
1054            "\n"
1055            "lookup TestLookup {\n"
1056            "    ignore pos leftGlyph [glyph1 glyph2]' rightGlyph;\n"
1057            "    pos [glyph1 glyph2]' lookup TestLookup_target;\n"
1058            "} TestLookup;\n",
1059        )
1060
1061    def test_def_anchor(self):
1062        fea = self.parse(
1063            'DEF_LOOKUP "TestLookup" PROCESS_BASE PROCESS_MARKS ALL '
1064            "DIRECTION LTR\n"
1065            "IN_CONTEXT\n"
1066            "END_CONTEXT\n"
1067            "AS_POSITION\n"
1068            'ATTACH GLYPH "a"\n'
1069            'TO GLYPH "acutecomb" AT ANCHOR "top"\n'
1070            "END_ATTACH\n"
1071            "END_POSITION\n"
1072            'DEF_ANCHOR "top" ON 120 GLYPH a '
1073            "COMPONENT 1 AT POS DX 250 DY 450 END_POS END_ANCHOR\n"
1074            'DEF_ANCHOR "MARK_top" ON 120 GLYPH acutecomb '
1075            "COMPONENT 1 AT POS DX 0 DY 450 END_POS END_ANCHOR"
1076        )
1077        self.assertEqual(
1078            fea,
1079            "\n# Mark classes\n"
1080            "markClass acutecomb <anchor 0 450> @top;\n"
1081            "\n"
1082            "# Lookups\n"
1083            "lookup TestLookup {\n"
1084            "    pos base a\n"
1085            "        <anchor 250 450> mark @top;\n"
1086            "} TestLookup;\n",
1087        )
1088
1089    def test_def_anchor_multi_component(self):
1090        fea = self.parse(
1091            'DEF_LOOKUP "TestLookup" PROCESS_BASE PROCESS_MARKS ALL '
1092            "DIRECTION LTR\n"
1093            "IN_CONTEXT\n"
1094            "END_CONTEXT\n"
1095            "AS_POSITION\n"
1096            'ATTACH GLYPH "f_f"\n'
1097            'TO GLYPH "acutecomb" AT ANCHOR "top"\n'
1098            "END_ATTACH\n"
1099            "END_POSITION\n"
1100            'DEF_GLYPH "f_f" ID 120 TYPE LIGATURE COMPONENTS 2 END_GLYPH\n'
1101            'DEF_ANCHOR "top" ON 120 GLYPH f_f '
1102            "COMPONENT 1 AT POS DX 250 DY 450 END_POS END_ANCHOR\n"
1103            'DEF_ANCHOR "top" ON 120 GLYPH f_f '
1104            "COMPONENT 2 AT POS DX 450 DY 450 END_POS END_ANCHOR\n"
1105            'DEF_ANCHOR "MARK_top" ON 120 GLYPH acutecomb '
1106            "COMPONENT 1 AT POS  END_POS END_ANCHOR"
1107        )
1108        self.assertEqual(
1109            fea,
1110            "\n# Mark classes\n"
1111            "markClass acutecomb <anchor 0 0> @top;\n"
1112            "\n"
1113            "# Lookups\n"
1114            "lookup TestLookup {\n"
1115            "    pos ligature f_f\n"
1116            "            <anchor 250 450> mark @top\n"
1117            "        ligComponent\n"
1118            "            <anchor 450 450> mark @top;\n"
1119            "} TestLookup;\n"
1120            "\n"
1121            "@GDEF_ligature = [f_f];\n"
1122            "table GDEF {\n"
1123            "    GlyphClassDef , @GDEF_ligature, , ;\n"
1124            "} GDEF;\n",
1125        )
1126
1127    def test_anchor_adjust_device(self):
1128        fea = self.parse(
1129            'DEF_ANCHOR "MARK_top" ON 123 GLYPH diacglyph '
1130            "COMPONENT 1 AT POS DX 0 DY 456 ADJUST_BY 12 AT 34 "
1131            "ADJUST_BY 56 AT 78 END_POS END_ANCHOR"
1132        )
1133        self.assertEqual(
1134            fea,
1135            "\n# Mark classes\n"
1136            "#markClass diacglyph <anchor 0 456 <device NULL>"
1137            " <device 34 12, 78 56>> @top;",
1138        )
1139
1140    def test_use_extension(self):
1141        fea = self.parse(
1142            'DEF_LOOKUP "kern1" PROCESS_BASE PROCESS_MARKS ALL '
1143            "DIRECTION LTR\n"
1144            "IN_CONTEXT\n"
1145            "END_CONTEXT\n"
1146            "AS_POSITION\n"
1147            "ADJUST_PAIR\n"
1148            ' FIRST GLYPH "A" FIRST GLYPH "V"\n'
1149            ' SECOND GLYPH "A" SECOND GLYPH "V"\n'
1150            " 1 2 BY POS ADV -30 END_POS POS END_POS\n"
1151            " 2 1 BY POS ADV -25 END_POS POS END_POS\n"
1152            "END_ADJUST\n"
1153            "END_POSITION\n"
1154            "COMPILER_USEEXTENSIONLOOKUPS\n"
1155        )
1156        self.assertEqual(
1157            fea,
1158            "\n# Lookups\n"
1159            "lookup kern1 useExtension {\n"
1160            "    enum pos A V -30;\n"
1161            "    enum pos V A -25;\n"
1162            "} kern1;\n",
1163        )
1164
1165    def test_unsupported_compiler_flags(self):
1166        with self.assertLogs(level="WARNING") as logs:
1167            fea = self.parse("CMAP_FORMAT 0 3 4")
1168            self.assertEqual(fea, "")
1169        self.assertEqual(
1170            logs.output,
1171            [
1172                "WARNING:fontTools.voltLib.voltToFea:Unsupported setting ignored: CMAP_FORMAT"
1173            ],
1174        )
1175
1176    def test_sanitize_lookup_name(self):
1177        fea = self.parse(
1178            'DEF_LOOKUP "Test Lookup" PROCESS_BASE PROCESS_MARKS ALL '
1179            "DIRECTION LTR IN_CONTEXT END_CONTEXT\n"
1180            "AS_POSITION ADJUST_PAIR END_ADJUST END_POSITION\n"
1181            'DEF_LOOKUP "Test-Lookup" PROCESS_BASE PROCESS_MARKS ALL '
1182            "DIRECTION LTR IN_CONTEXT END_CONTEXT\n"
1183            "AS_POSITION ADJUST_PAIR END_ADJUST END_POSITION\n"
1184        )
1185        self.assertEqual(
1186            fea,
1187            "\n# Lookups\n"
1188            "lookup Test_Lookup {\n"
1189            "    \n"
1190            "} Test_Lookup;\n"
1191            "\n"
1192            "lookup Test_Lookup_ {\n"
1193            "    \n"
1194            "} Test_Lookup_;\n",
1195        )
1196
1197    def test_sanitize_group_name(self):
1198        fea = self.parse(
1199            'DEF_GROUP "aaccented glyphs"\n'
1200            'ENUM GLYPH "aacute" GLYPH "abreve" END_ENUM\n'
1201            "END_GROUP\n"
1202            'DEF_GROUP "aaccented+glyphs"\n'
1203            'ENUM GLYPH "aacute" GLYPH "abreve" END_ENUM\n'
1204            "END_GROUP\n"
1205        )
1206        self.assertEqual(
1207            fea,
1208            "# Glyph classes\n"
1209            "@aaccented_glyphs = [aacute abreve];\n"
1210            "@aaccented_glyphs_ = [aacute abreve];",
1211        )
1212
1213    def test_cli_vtp(self):
1214        vtp = DATADIR / "Nutso.vtp"
1215        fea = DATADIR / "Nutso.fea"
1216        self.cli(vtp, fea)
1217
1218    def test_group_order(self):
1219        vtp = DATADIR / "NamdhinggoSIL1006.vtp"
1220        fea = DATADIR / "NamdhinggoSIL1006.fea"
1221        self.cli(vtp, fea)
1222
1223    def test_cli_ttf(self):
1224        ttf = DATADIR / "Nutso.ttf"
1225        fea = DATADIR / "Nutso.fea"
1226        self.cli(ttf, fea)
1227
1228    def test_cli_ttf_no_TSIV(self):
1229        from fontTools.voltLib.voltToFea import main as cli
1230
1231        ttf = DATADIR / "Empty.ttf"
1232        temp = self.temp_path()
1233        self.assertEqual(1, cli([str(ttf), str(temp)]))
1234
1235    def cli(self, source, fea):
1236        from fontTools.voltLib.voltToFea import main as cli
1237
1238        temp = self.temp_path()
1239        cli([str(source), str(temp)])
1240        with temp.open() as f:
1241            res = f.read()
1242        with fea.open() as f:
1243            ref = f.read()
1244        self.assertEqual(ref, res)
1245
1246    def parse(self, text):
1247        return VoltToFea(StringIO(text)).convert()
1248
1249
1250if __name__ == "__main__":
1251    import sys
1252
1253    sys.exit(unittest.main())
1254