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