1from fontTools.voltLib import ast 2from fontTools.voltLib.error import VoltLibError 3from fontTools.voltLib.parser import Parser 4from io import StringIO 5import unittest 6 7 8class ParserTest(unittest.TestCase): 9 def __init__(self, methodName): 10 unittest.TestCase.__init__(self, methodName) 11 # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, 12 # and fires deprecation warnings if a program uses the old name. 13 if not hasattr(self, "assertRaisesRegex"): 14 self.assertRaisesRegex = self.assertRaisesRegexp 15 16 def assertSubEqual(self, sub, glyph_ref, replacement_ref): 17 glyphs = [[g.glyph for g in v] for v in sub.mapping.keys()] 18 replacement = [[g.glyph for g in v] for v in sub.mapping.values()] 19 20 self.assertEqual(glyphs, glyph_ref) 21 self.assertEqual(replacement, replacement_ref) 22 23 def test_def_glyph_base(self): 24 [def_glyph] = self.parse( 25 'DEF_GLYPH ".notdef" ID 0 TYPE BASE END_GLYPH' 26 ).statements 27 self.assertEqual( 28 ( 29 def_glyph.name, 30 def_glyph.id, 31 def_glyph.unicode, 32 def_glyph.type, 33 def_glyph.components, 34 ), 35 (".notdef", 0, None, "BASE", None), 36 ) 37 38 def test_def_glyph_base_with_unicode(self): 39 [def_glyph] = self.parse( 40 'DEF_GLYPH "space" ID 3 UNICODE 32 TYPE BASE END_GLYPH' 41 ).statements 42 self.assertEqual( 43 ( 44 def_glyph.name, 45 def_glyph.id, 46 def_glyph.unicode, 47 def_glyph.type, 48 def_glyph.components, 49 ), 50 ("space", 3, [0x0020], "BASE", None), 51 ) 52 53 def test_def_glyph_base_with_unicodevalues(self): 54 [def_glyph] = self.parse_( 55 'DEF_GLYPH "CR" ID 2 UNICODEVALUES "U+0009" ' "TYPE BASE END_GLYPH" 56 ).statements 57 self.assertEqual( 58 ( 59 def_glyph.name, 60 def_glyph.id, 61 def_glyph.unicode, 62 def_glyph.type, 63 def_glyph.components, 64 ), 65 ("CR", 2, [0x0009], "BASE", None), 66 ) 67 68 def test_def_glyph_base_with_mult_unicodevalues(self): 69 [def_glyph] = self.parse( 70 'DEF_GLYPH "CR" ID 2 UNICODEVALUES "U+0009,U+000D" ' "TYPE BASE END_GLYPH" 71 ).statements 72 self.assertEqual( 73 ( 74 def_glyph.name, 75 def_glyph.id, 76 def_glyph.unicode, 77 def_glyph.type, 78 def_glyph.components, 79 ), 80 ("CR", 2, [0x0009, 0x000D], "BASE", None), 81 ) 82 83 def test_def_glyph_base_with_empty_unicodevalues(self): 84 [def_glyph] = self.parse_( 85 'DEF_GLYPH "i.locl" ID 269 UNICODEVALUES "" ' "TYPE BASE END_GLYPH" 86 ).statements 87 self.assertEqual( 88 ( 89 def_glyph.name, 90 def_glyph.id, 91 def_glyph.unicode, 92 def_glyph.type, 93 def_glyph.components, 94 ), 95 ("i.locl", 269, None, "BASE", None), 96 ) 97 98 def test_def_glyph_base_2_components(self): 99 [def_glyph] = self.parse( 100 'DEF_GLYPH "glyphBase" ID 320 TYPE BASE COMPONENTS 2 END_GLYPH' 101 ).statements 102 self.assertEqual( 103 ( 104 def_glyph.name, 105 def_glyph.id, 106 def_glyph.unicode, 107 def_glyph.type, 108 def_glyph.components, 109 ), 110 ("glyphBase", 320, None, "BASE", 2), 111 ) 112 113 def test_def_glyph_ligature_2_components(self): 114 [def_glyph] = self.parse( 115 'DEF_GLYPH "f_f" ID 320 TYPE LIGATURE COMPONENTS 2 END_GLYPH' 116 ).statements 117 self.assertEqual( 118 ( 119 def_glyph.name, 120 def_glyph.id, 121 def_glyph.unicode, 122 def_glyph.type, 123 def_glyph.components, 124 ), 125 ("f_f", 320, None, "LIGATURE", 2), 126 ) 127 128 def test_def_glyph_mark(self): 129 [def_glyph] = self.parse( 130 'DEF_GLYPH "brevecomb" ID 320 TYPE MARK END_GLYPH' 131 ).statements 132 self.assertEqual( 133 ( 134 def_glyph.name, 135 def_glyph.id, 136 def_glyph.unicode, 137 def_glyph.type, 138 def_glyph.components, 139 ), 140 ("brevecomb", 320, None, "MARK", None), 141 ) 142 143 def test_def_glyph_component(self): 144 [def_glyph] = self.parse( 145 'DEF_GLYPH "f.f_f" ID 320 TYPE COMPONENT END_GLYPH' 146 ).statements 147 self.assertEqual( 148 ( 149 def_glyph.name, 150 def_glyph.id, 151 def_glyph.unicode, 152 def_glyph.type, 153 def_glyph.components, 154 ), 155 ("f.f_f", 320, None, "COMPONENT", None), 156 ) 157 158 def test_def_glyph_no_type(self): 159 [def_glyph] = self.parse('DEF_GLYPH "glyph20" ID 20 END_GLYPH').statements 160 self.assertEqual( 161 ( 162 def_glyph.name, 163 def_glyph.id, 164 def_glyph.unicode, 165 def_glyph.type, 166 def_glyph.components, 167 ), 168 ("glyph20", 20, None, None, None), 169 ) 170 171 def test_def_glyph_case_sensitive(self): 172 def_glyphs = self.parse( 173 'DEF_GLYPH "A" ID 3 UNICODE 65 TYPE BASE END_GLYPH\n' 174 'DEF_GLYPH "a" ID 4 UNICODE 97 TYPE BASE END_GLYPH' 175 ).statements 176 self.assertEqual( 177 ( 178 def_glyphs[0].name, 179 def_glyphs[0].id, 180 def_glyphs[0].unicode, 181 def_glyphs[0].type, 182 def_glyphs[0].components, 183 ), 184 ("A", 3, [0x41], "BASE", None), 185 ) 186 self.assertEqual( 187 ( 188 def_glyphs[1].name, 189 def_glyphs[1].id, 190 def_glyphs[1].unicode, 191 def_glyphs[1].type, 192 def_glyphs[1].components, 193 ), 194 ("a", 4, [0x61], "BASE", None), 195 ) 196 197 def test_def_group_glyphs(self): 198 [def_group] = self.parse( 199 'DEF_GROUP "aaccented"\n' 200 ' ENUM GLYPH "aacute" GLYPH "abreve" GLYPH "acircumflex" ' 201 'GLYPH "adieresis" GLYPH "ae" GLYPH "agrave" GLYPH "amacron" ' 202 'GLYPH "aogonek" GLYPH "aring" GLYPH "atilde" END_ENUM\n' 203 "END_GROUP" 204 ).statements 205 self.assertEqual( 206 (def_group.name, def_group.enum.glyphSet()), 207 ( 208 "aaccented", 209 ( 210 "aacute", 211 "abreve", 212 "acircumflex", 213 "adieresis", 214 "ae", 215 "agrave", 216 "amacron", 217 "aogonek", 218 "aring", 219 "atilde", 220 ), 221 ), 222 ) 223 224 def test_def_group_groups(self): 225 [group1, group2, test_group] = self.parse( 226 'DEF_GROUP "Group1"\n' 227 ' ENUM GLYPH "a" GLYPH "b" GLYPH "c" GLYPH "d" END_ENUM\n' 228 "END_GROUP\n" 229 'DEF_GROUP "Group2"\n' 230 ' ENUM GLYPH "e" GLYPH "f" GLYPH "g" GLYPH "h" END_ENUM\n' 231 "END_GROUP\n" 232 'DEF_GROUP "TestGroup"\n' 233 ' ENUM GROUP "Group1" GROUP "Group2" END_ENUM\n' 234 "END_GROUP" 235 ).statements 236 groups = [g.group for g in test_group.enum.enum] 237 self.assertEqual((test_group.name, groups), ("TestGroup", ["Group1", "Group2"])) 238 239 def test_def_group_groups_not_yet_defined(self): 240 [group1, test_group1, test_group2, test_group3, group2] = self.parse( 241 'DEF_GROUP "Group1"\n' 242 ' ENUM GLYPH "a" GLYPH "b" GLYPH "c" GLYPH "d" END_ENUM\n' 243 "END_GROUP\n" 244 'DEF_GROUP "TestGroup1"\n' 245 ' ENUM GROUP "Group1" GROUP "Group2" END_ENUM\n' 246 "END_GROUP\n" 247 'DEF_GROUP "TestGroup2"\n' 248 ' ENUM GROUP "Group2" END_ENUM\n' 249 "END_GROUP\n" 250 'DEF_GROUP "TestGroup3"\n' 251 ' ENUM GROUP "Group2" GROUP "Group1" END_ENUM\n' 252 "END_GROUP\n" 253 'DEF_GROUP "Group2"\n' 254 ' ENUM GLYPH "e" GLYPH "f" GLYPH "g" GLYPH "h" END_ENUM\n' 255 "END_GROUP" 256 ).statements 257 groups = [g.group for g in test_group1.enum.enum] 258 self.assertEqual( 259 (test_group1.name, groups), ("TestGroup1", ["Group1", "Group2"]) 260 ) 261 groups = [g.group for g in test_group2.enum.enum] 262 self.assertEqual((test_group2.name, groups), ("TestGroup2", ["Group2"])) 263 groups = [g.group for g in test_group3.enum.enum] 264 self.assertEqual( 265 (test_group3.name, groups), ("TestGroup3", ["Group2", "Group1"]) 266 ) 267 268 # def test_def_group_groups_undefined(self): 269 # with self.assertRaisesRegex( 270 # VoltLibError, 271 # r'Group "Group2" is used but undefined.'): 272 # [group1, test_group, group2] = self.parse( 273 # 'DEF_GROUP "Group1"\n' 274 # 'ENUM GLYPH "a" GLYPH "b" GLYPH "c" GLYPH "d" END_ENUM\n' 275 # 'END_GROUP\n' 276 # 'DEF_GROUP "TestGroup"\n' 277 # 'ENUM GROUP "Group1" GROUP "Group2" END_ENUM\n' 278 # 'END_GROUP\n' 279 # ).statements 280 281 def test_def_group_glyphs_and_group(self): 282 [def_group1, def_group2] = self.parse( 283 'DEF_GROUP "aaccented"\n' 284 ' ENUM GLYPH "aacute" GLYPH "abreve" GLYPH "acircumflex" ' 285 'GLYPH "adieresis" GLYPH "ae" GLYPH "agrave" GLYPH "amacron" ' 286 'GLYPH "aogonek" GLYPH "aring" GLYPH "atilde" END_ENUM\n' 287 "END_GROUP\n" 288 'DEF_GROUP "KERN_lc_a_2ND"\n' 289 ' ENUM GLYPH "a" GROUP "aaccented" END_ENUM\n' 290 "END_GROUP" 291 ).statements 292 items = def_group2.enum.enum 293 self.assertEqual( 294 (def_group2.name, items[0].glyphSet(), items[1].group), 295 ("KERN_lc_a_2ND", ("a",), "aaccented"), 296 ) 297 298 def test_def_group_range(self): 299 def_group = self.parse( 300 'DEF_GLYPH "a" ID 163 UNICODE 97 TYPE BASE END_GLYPH\n' 301 'DEF_GLYPH "agrave" ID 194 UNICODE 224 TYPE BASE END_GLYPH\n' 302 'DEF_GLYPH "aacute" ID 195 UNICODE 225 TYPE BASE END_GLYPH\n' 303 'DEF_GLYPH "acircumflex" ID 196 UNICODE 226 TYPE BASE END_GLYPH\n' 304 'DEF_GLYPH "atilde" ID 197 UNICODE 227 TYPE BASE END_GLYPH\n' 305 'DEF_GLYPH "c" ID 165 UNICODE 99 TYPE BASE END_GLYPH\n' 306 'DEF_GLYPH "ccaron" ID 209 UNICODE 269 TYPE BASE END_GLYPH\n' 307 'DEF_GLYPH "ccedilla" ID 210 UNICODE 231 TYPE BASE END_GLYPH\n' 308 'DEF_GLYPH "cdotaccent" ID 210 UNICODE 267 TYPE BASE END_GLYPH\n' 309 'DEF_GROUP "KERN_lc_a_2ND"\n' 310 ' ENUM RANGE "a" TO "atilde" GLYPH "b" RANGE "c" TO "cdotaccent" ' 311 "END_ENUM\n" 312 "END_GROUP" 313 ).statements[-1] 314 self.assertEqual( 315 (def_group.name, def_group.enum.glyphSet()), 316 ( 317 "KERN_lc_a_2ND", 318 ( 319 "a", 320 "agrave", 321 "aacute", 322 "acircumflex", 323 "atilde", 324 "b", 325 "c", 326 "ccaron", 327 "ccedilla", 328 "cdotaccent", 329 ), 330 ), 331 ) 332 333 def test_group_duplicate(self): 334 self.assertRaisesRegex( 335 VoltLibError, 336 'Glyph group "dupe" already defined, ' "group names are case insensitive", 337 self.parse, 338 'DEF_GROUP "dupe"\n' 339 'ENUM GLYPH "a" GLYPH "b" END_ENUM\n' 340 "END_GROUP\n" 341 'DEF_GROUP "dupe"\n' 342 'ENUM GLYPH "x" END_ENUM\n' 343 "END_GROUP", 344 ) 345 346 def test_group_duplicate_case_insensitive(self): 347 self.assertRaisesRegex( 348 VoltLibError, 349 'Glyph group "Dupe" already defined, ' "group names are case insensitive", 350 self.parse, 351 'DEF_GROUP "dupe"\n' 352 'ENUM GLYPH "a" GLYPH "b" END_ENUM\n' 353 "END_GROUP\n" 354 'DEF_GROUP "Dupe"\n' 355 'ENUM GLYPH "x" END_ENUM\n' 356 "END_GROUP", 357 ) 358 359 def test_script_without_langsys(self): 360 [script] = self.parse( 361 'DEF_SCRIPT NAME "Latin" TAG "latn"\n\n' "END_SCRIPT" 362 ).statements 363 self.assertEqual((script.name, script.tag, script.langs), ("Latin", "latn", [])) 364 365 def test_langsys_normal(self): 366 [def_script] = self.parse( 367 'DEF_SCRIPT NAME "Latin" TAG "latn"\n\n' 368 'DEF_LANGSYS NAME "Romanian" TAG "ROM "\n\n' 369 "END_LANGSYS\n" 370 'DEF_LANGSYS NAME "Moldavian" TAG "MOL "\n\n' 371 "END_LANGSYS\n" 372 "END_SCRIPT" 373 ).statements 374 self.assertEqual((def_script.name, def_script.tag), ("Latin", "latn")) 375 def_lang = def_script.langs[0] 376 self.assertEqual((def_lang.name, def_lang.tag), ("Romanian", "ROM ")) 377 def_lang = def_script.langs[1] 378 self.assertEqual((def_lang.name, def_lang.tag), ("Moldavian", "MOL ")) 379 380 def test_langsys_no_script_name(self): 381 [langsys] = self.parse( 382 'DEF_SCRIPT TAG "latn"\n\n' 383 'DEF_LANGSYS NAME "Default" TAG "dflt"\n\n' 384 "END_LANGSYS\n" 385 "END_SCRIPT" 386 ).statements 387 self.assertEqual((langsys.name, langsys.tag), (None, "latn")) 388 lang = langsys.langs[0] 389 self.assertEqual((lang.name, lang.tag), ("Default", "dflt")) 390 391 def test_langsys_no_script_tag_fails(self): 392 with self.assertRaisesRegex(VoltLibError, r'.*Expected "TAG"'): 393 [langsys] = self.parse( 394 'DEF_SCRIPT NAME "Latin"\n\n' 395 'DEF_LANGSYS NAME "Default" TAG "dflt"\n\n' 396 "END_LANGSYS\n" 397 "END_SCRIPT" 398 ).statements 399 400 def test_langsys_duplicate_script(self): 401 with self.assertRaisesRegex( 402 VoltLibError, 403 'Script "DFLT" already defined, ' "script tags are case insensitive", 404 ): 405 [langsys1, langsys2] = self.parse( 406 'DEF_SCRIPT NAME "Default" TAG "DFLT"\n\n' 407 'DEF_LANGSYS NAME "Default" TAG "dflt"\n\n' 408 "END_LANGSYS\n" 409 "END_SCRIPT\n" 410 'DEF_SCRIPT TAG "DFLT"\n\n' 411 'DEF_LANGSYS NAME "Default" TAG "dflt"\n\n' 412 "END_LANGSYS\n" 413 "END_SCRIPT" 414 ).statements 415 416 def test_langsys_duplicate_lang(self): 417 with self.assertRaisesRegex( 418 VoltLibError, 419 'Language "dflt" already defined in script "DFLT", ' 420 "language tags are case insensitive", 421 ): 422 [langsys] = self.parse( 423 'DEF_SCRIPT NAME "Default" TAG "DFLT"\n' 424 'DEF_LANGSYS NAME "Default" TAG "dflt"\n' 425 "END_LANGSYS\n" 426 'DEF_LANGSYS NAME "Default" TAG "dflt"\n' 427 "END_LANGSYS\n" 428 "END_SCRIPT" 429 ).statements 430 431 def test_langsys_lang_in_separate_scripts(self): 432 [langsys1, langsys2] = self.parse( 433 'DEF_SCRIPT NAME "Default" TAG "DFLT"\n\n' 434 'DEF_LANGSYS NAME "Default" TAG "dflt"\n\n' 435 "END_LANGSYS\n" 436 'DEF_LANGSYS NAME "Default" TAG "ROM "\n\n' 437 "END_LANGSYS\n" 438 "END_SCRIPT\n" 439 'DEF_SCRIPT NAME "Latin" TAG "latn"\n\n' 440 'DEF_LANGSYS NAME "Default" TAG "dflt"\n\n' 441 "END_LANGSYS\n" 442 'DEF_LANGSYS NAME "Default" TAG "ROM "\n\n' 443 "END_LANGSYS\n" 444 "END_SCRIPT" 445 ).statements 446 self.assertEqual( 447 (langsys1.langs[0].tag, langsys1.langs[1].tag), ("dflt", "ROM ") 448 ) 449 self.assertEqual( 450 (langsys2.langs[0].tag, langsys2.langs[1].tag), ("dflt", "ROM ") 451 ) 452 453 def test_langsys_no_lang_name(self): 454 [langsys] = self.parse( 455 'DEF_SCRIPT NAME "Latin" TAG "latn"\n\n' 456 'DEF_LANGSYS TAG "dflt"\n\n' 457 "END_LANGSYS\n" 458 "END_SCRIPT" 459 ).statements 460 self.assertEqual((langsys.name, langsys.tag), ("Latin", "latn")) 461 lang = langsys.langs[0] 462 self.assertEqual((lang.name, lang.tag), (None, "dflt")) 463 464 def test_langsys_no_langsys_tag_fails(self): 465 with self.assertRaisesRegex(VoltLibError, r'.*Expected "TAG"'): 466 [langsys] = self.parse( 467 'DEF_SCRIPT NAME "Latin" TAG "latn"\n\n' 468 'DEF_LANGSYS NAME "Default"\n\n' 469 "END_LANGSYS\n" 470 "END_SCRIPT" 471 ).statements 472 473 def test_feature(self): 474 [def_script] = self.parse( 475 'DEF_SCRIPT NAME "Latin" TAG "latn"\n\n' 476 'DEF_LANGSYS NAME "Romanian" TAG "ROM "\n\n' 477 'DEF_FEATURE NAME "Fractions" TAG "frac"\n' 478 ' LOOKUP "fraclookup"\n' 479 "END_FEATURE\n" 480 "END_LANGSYS\n" 481 "END_SCRIPT" 482 ).statements 483 def_feature = def_script.langs[0].features[0] 484 self.assertEqual( 485 (def_feature.name, def_feature.tag, def_feature.lookups), 486 ("Fractions", "frac", ["fraclookup"]), 487 ) 488 [def_script] = self.parse( 489 'DEF_SCRIPT NAME "Latin" TAG "latn"\n\n' 490 'DEF_LANGSYS NAME "Romanian" TAG "ROM "\n\n' 491 'DEF_FEATURE NAME "Kerning" TAG "kern"\n' 492 ' LOOKUP "kern1" LOOKUP "kern2"\n' 493 "END_FEATURE\n" 494 "END_LANGSYS\n" 495 "END_SCRIPT" 496 ).statements 497 def_feature = def_script.langs[0].features[0] 498 self.assertEqual( 499 (def_feature.name, def_feature.tag, def_feature.lookups), 500 ("Kerning", "kern", ["kern1", "kern2"]), 501 ) 502 503 def test_lookup_duplicate(self): 504 with self.assertRaisesRegex( 505 VoltLibError, 506 'Lookup "dupe" already defined, ' "lookup names are case insensitive", 507 ): 508 [lookup1, lookup2] = self.parse( 509 'DEF_LOOKUP "dupe"\n' 510 "AS_SUBSTITUTION\n" 511 'SUB GLYPH "a"\n' 512 'WITH GLYPH "a.alt"\n' 513 "END_SUB\n" 514 "END_SUBSTITUTION\n" 515 'DEF_LOOKUP "dupe"\n' 516 "AS_SUBSTITUTION\n" 517 'SUB GLYPH "b"\n' 518 'WITH GLYPH "b.alt"\n' 519 "END_SUB\n" 520 "END_SUBSTITUTION\n" 521 ).statements 522 523 def test_lookup_duplicate_insensitive_case(self): 524 with self.assertRaisesRegex( 525 VoltLibError, 526 'Lookup "Dupe" already defined, ' "lookup names are case insensitive", 527 ): 528 [lookup1, lookup2] = self.parse( 529 'DEF_LOOKUP "dupe"\n' 530 "AS_SUBSTITUTION\n" 531 'SUB GLYPH "a"\n' 532 'WITH GLYPH "a.alt"\n' 533 "END_SUB\n" 534 "END_SUBSTITUTION\n" 535 'DEF_LOOKUP "Dupe"\n' 536 "AS_SUBSTITUTION\n" 537 'SUB GLYPH "b"\n' 538 'WITH GLYPH "b.alt"\n' 539 "END_SUB\n" 540 "END_SUBSTITUTION\n" 541 ).statements 542 543 def test_lookup_name_starts_with_letter(self): 544 with self.assertRaisesRegex( 545 VoltLibError, r'Lookup name "\\lookupname" must start with a letter' 546 ): 547 [lookup] = self.parse( 548 'DEF_LOOKUP "\\lookupname"\n' 549 "AS_SUBSTITUTION\n" 550 'SUB GLYPH "a"\n' 551 'WITH GLYPH "a.alt"\n' 552 "END_SUB\n" 553 "END_SUBSTITUTION\n" 554 ).statements 555 556 def test_lookup_comments(self): 557 [lookup] = self.parse( 558 'DEF_LOOKUP "test" PROCESS_BASE PROCESS_MARKS ALL DIRECTION LTR\n' 559 'COMMENTS "Hello\\nWorld"\n' 560 "IN_CONTEXT\n" 561 "END_CONTEXT\n" 562 "AS_SUBSTITUTION\n" 563 'SUB GLYPH "a"\n' 564 'WITH GLYPH "b"\n' 565 "END_SUB\n" 566 "END_SUBSTITUTION" 567 ).statements 568 self.assertEqual(lookup.name, "test") 569 self.assertEqual(lookup.comments, "Hello\nWorld") 570 571 def test_substitution_empty(self): 572 with self.assertRaisesRegex(VoltLibError, r"Expected SUB"): 573 [lookup] = self.parse( 574 'DEF_LOOKUP "empty_substitution" PROCESS_BASE PROCESS_MARKS ' 575 "ALL DIRECTION LTR\n" 576 "IN_CONTEXT\n" 577 "END_CONTEXT\n" 578 "AS_SUBSTITUTION\n" 579 "END_SUBSTITUTION" 580 ).statements 581 582 def test_substitution_invalid_many_to_many(self): 583 with self.assertRaisesRegex(VoltLibError, r"Invalid substitution type"): 584 [lookup] = self.parse( 585 'DEF_LOOKUP "invalid_substitution" PROCESS_BASE PROCESS_MARKS ' 586 "ALL DIRECTION LTR\n" 587 "IN_CONTEXT\n" 588 "END_CONTEXT\n" 589 "AS_SUBSTITUTION\n" 590 'SUB GLYPH "f" GLYPH "i"\n' 591 'WITH GLYPH "f.alt" GLYPH "i.alt"\n' 592 "END_SUB\n" 593 "END_SUBSTITUTION" 594 ).statements 595 596 def test_substitution_invalid_reverse_chaining_single(self): 597 with self.assertRaisesRegex(VoltLibError, r"Invalid substitution type"): 598 [lookup] = self.parse( 599 'DEF_LOOKUP "invalid_substitution" PROCESS_BASE PROCESS_MARKS ' 600 "ALL DIRECTION LTR REVERSAL\n" 601 "IN_CONTEXT\n" 602 "END_CONTEXT\n" 603 "AS_SUBSTITUTION\n" 604 'SUB GLYPH "f" GLYPH "i"\n' 605 'WITH GLYPH "f_i"\n' 606 "END_SUB\n" 607 "END_SUBSTITUTION" 608 ).statements 609 610 def test_substitution_invalid_mixed(self): 611 with self.assertRaisesRegex(VoltLibError, r"Invalid substitution type"): 612 [lookup] = self.parse( 613 'DEF_LOOKUP "invalid_substitution" PROCESS_BASE PROCESS_MARKS ' 614 "ALL DIRECTION LTR\n" 615 "IN_CONTEXT\n" 616 "END_CONTEXT\n" 617 "AS_SUBSTITUTION\n" 618 'SUB GLYPH "fi"\n' 619 'WITH GLYPH "f" GLYPH "i"\n' 620 "END_SUB\n" 621 'SUB GLYPH "f" GLYPH "l"\n' 622 'WITH GLYPH "f_l"\n' 623 "END_SUB\n" 624 "END_SUBSTITUTION" 625 ).statements 626 627 def test_substitution_single(self): 628 [lookup] = self.parse( 629 'DEF_LOOKUP "smcp" PROCESS_BASE PROCESS_MARKS ALL ' 630 "DIRECTION LTR\n" 631 "IN_CONTEXT\n" 632 "END_CONTEXT\n" 633 "AS_SUBSTITUTION\n" 634 'SUB GLYPH "a"\n' 635 'WITH GLYPH "a.sc"\n' 636 "END_SUB\n" 637 'SUB GLYPH "b"\n' 638 'WITH GLYPH "b.sc"\n' 639 "END_SUB\n" 640 "END_SUBSTITUTION" 641 ).statements 642 self.assertEqual(lookup.name, "smcp") 643 self.assertSubEqual(lookup.sub, [["a"], ["b"]], [["a.sc"], ["b.sc"]]) 644 645 def test_substitution_single_in_context(self): 646 [group, lookup] = self.parse( 647 'DEF_GROUP "Denominators"\n' 648 ' ENUM GLYPH "one.dnom" GLYPH "two.dnom" END_ENUM\n' 649 "END_GROUP\n" 650 'DEF_LOOKUP "fracdnom" PROCESS_BASE PROCESS_MARKS ALL ' 651 "DIRECTION LTR\n" 652 "IN_CONTEXT\n" 653 ' LEFT ENUM GROUP "Denominators" GLYPH "fraction" END_ENUM\n' 654 "END_CONTEXT\n" 655 "AS_SUBSTITUTION\n" 656 'SUB GLYPH "one"\n' 657 'WITH GLYPH "one.dnom"\n' 658 "END_SUB\n" 659 'SUB GLYPH "two"\n' 660 'WITH GLYPH "two.dnom"\n' 661 "END_SUB\n" 662 "END_SUBSTITUTION" 663 ).statements 664 context = lookup.context[0] 665 666 self.assertEqual(lookup.name, "fracdnom") 667 self.assertEqual(context.ex_or_in, "IN_CONTEXT") 668 self.assertEqual(len(context.left), 1) 669 self.assertEqual(len(context.left[0]), 1) 670 self.assertEqual(len(context.left[0][0].enum), 2) 671 self.assertEqual(context.left[0][0].enum[0].group, "Denominators") 672 self.assertEqual(context.left[0][0].enum[1].glyph, "fraction") 673 self.assertEqual(context.right, []) 674 self.assertSubEqual( 675 lookup.sub, [["one"], ["two"]], [["one.dnom"], ["two.dnom"]] 676 ) 677 678 def test_substitution_single_in_contexts(self): 679 [group, lookup] = self.parse( 680 'DEF_GROUP "Hebrew"\n' 681 ' ENUM GLYPH "uni05D0" GLYPH "uni05D1" END_ENUM\n' 682 "END_GROUP\n" 683 'DEF_LOOKUP "HebrewCurrency" PROCESS_BASE PROCESS_MARKS ALL ' 684 "DIRECTION LTR\n" 685 "IN_CONTEXT\n" 686 ' RIGHT GROUP "Hebrew"\n' 687 ' RIGHT GLYPH "one.Hebr"\n' 688 "END_CONTEXT\n" 689 "IN_CONTEXT\n" 690 ' LEFT GROUP "Hebrew"\n' 691 ' LEFT GLYPH "one.Hebr"\n' 692 "END_CONTEXT\n" 693 "AS_SUBSTITUTION\n" 694 'SUB GLYPH "dollar"\n' 695 'WITH GLYPH "dollar.Hebr"\n' 696 "END_SUB\n" 697 "END_SUBSTITUTION" 698 ).statements 699 context1 = lookup.context[0] 700 context2 = lookup.context[1] 701 702 self.assertEqual(lookup.name, "HebrewCurrency") 703 704 self.assertEqual(context1.ex_or_in, "IN_CONTEXT") 705 self.assertEqual(context1.left, []) 706 self.assertEqual(len(context1.right), 2) 707 self.assertEqual(len(context1.right[0]), 1) 708 self.assertEqual(len(context1.right[1]), 1) 709 self.assertEqual(context1.right[0][0].group, "Hebrew") 710 self.assertEqual(context1.right[1][0].glyph, "one.Hebr") 711 712 self.assertEqual(context2.ex_or_in, "IN_CONTEXT") 713 self.assertEqual(len(context2.left), 2) 714 self.assertEqual(len(context2.left[0]), 1) 715 self.assertEqual(len(context2.left[1]), 1) 716 self.assertEqual(context2.left[0][0].group, "Hebrew") 717 self.assertEqual(context2.left[1][0].glyph, "one.Hebr") 718 self.assertEqual(context2.right, []) 719 720 def test_substitution_skip_base(self): 721 [group, lookup] = self.parse( 722 'DEF_GROUP "SomeMarks"\n' 723 ' ENUM GLYPH "marka" GLYPH "markb" END_ENUM\n' 724 "END_GROUP\n" 725 'DEF_LOOKUP "SomeSub" SKIP_BASE PROCESS_MARKS ALL ' 726 "DIRECTION LTR\n" 727 "IN_CONTEXT\n" 728 "END_CONTEXT\n" 729 "AS_SUBSTITUTION\n" 730 'SUB GLYPH "A"\n' 731 'WITH GLYPH "A.c2sc"\n' 732 "END_SUB\n" 733 "END_SUBSTITUTION" 734 ).statements 735 self.assertEqual((lookup.name, lookup.process_base), ("SomeSub", False)) 736 737 def test_substitution_process_base(self): 738 [group, lookup] = self.parse( 739 'DEF_GROUP "SomeMarks"\n' 740 ' ENUM GLYPH "marka" GLYPH "markb" END_ENUM\n' 741 "END_GROUP\n" 742 'DEF_LOOKUP "SomeSub" PROCESS_BASE PROCESS_MARKS ALL ' 743 "DIRECTION LTR\n" 744 "IN_CONTEXT\n" 745 "END_CONTEXT\n" 746 "AS_SUBSTITUTION\n" 747 'SUB GLYPH "A"\n' 748 'WITH GLYPH "A.c2sc"\n' 749 "END_SUB\n" 750 "END_SUBSTITUTION" 751 ).statements 752 self.assertEqual((lookup.name, lookup.process_base), ("SomeSub", True)) 753 754 def test_substitution_process_marks(self): 755 [group, lookup] = self.parse( 756 'DEF_GROUP "SomeMarks"\n' 757 ' ENUM GLYPH "marka" GLYPH "markb" END_ENUM\n' 758 "END_GROUP\n" 759 'DEF_LOOKUP "SomeSub" PROCESS_BASE PROCESS_MARKS "SomeMarks"\n' 760 "IN_CONTEXT\n" 761 "END_CONTEXT\n" 762 "AS_SUBSTITUTION\n" 763 'SUB GLYPH "A"\n' 764 'WITH GLYPH "A.c2sc"\n' 765 "END_SUB\n" 766 "END_SUBSTITUTION" 767 ).statements 768 self.assertEqual((lookup.name, lookup.process_marks), ("SomeSub", "SomeMarks")) 769 770 def test_substitution_process_marks_all(self): 771 [lookup] = self.parse( 772 'DEF_LOOKUP "SomeSub" PROCESS_BASE PROCESS_MARKS ALL\n' 773 "IN_CONTEXT\n" 774 "END_CONTEXT\n" 775 "AS_SUBSTITUTION\n" 776 'SUB GLYPH "A"\n' 777 'WITH GLYPH "A.c2sc"\n' 778 "END_SUB\n" 779 "END_SUBSTITUTION" 780 ).statements 781 self.assertEqual((lookup.name, lookup.process_marks), ("SomeSub", True)) 782 783 def test_substitution_process_marks_none(self): 784 [lookup] = self.parse_( 785 'DEF_LOOKUP "SomeSub" PROCESS_BASE PROCESS_MARKS "NONE"\n' 786 "IN_CONTEXT\n" 787 "END_CONTEXT\n" 788 "AS_SUBSTITUTION\n" 789 'SUB GLYPH "A"\n' 790 'WITH GLYPH "A.c2sc"\n' 791 "END_SUB\n" 792 "END_SUBSTITUTION" 793 ).statements 794 self.assertEqual((lookup.name, lookup.process_marks), ("SomeSub", False)) 795 796 def test_substitution_process_marks_bad(self): 797 with self.assertRaisesRegex( 798 VoltLibError, "Expected ALL, NONE, MARK_GLYPH_SET or an ID" 799 ): 800 self.parse( 801 'DEF_GROUP "SomeMarks" ENUM GLYPH "marka" GLYPH "markb" ' 802 "END_ENUM END_GROUP\n" 803 'DEF_LOOKUP "SomeSub" PROCESS_BASE PROCESS_MARKS SomeMarks ' 804 "AS_SUBSTITUTION\n" 805 'SUB GLYPH "A" WITH GLYPH "A.c2sc"\n' 806 "END_SUB\n" 807 "END_SUBSTITUTION" 808 ) 809 810 def test_substitution_skip_marks(self): 811 [group, lookup] = self.parse( 812 'DEF_GROUP "SomeMarks"\n' 813 ' ENUM GLYPH "marka" GLYPH "markb" END_ENUM\n' 814 "END_GROUP\n" 815 'DEF_LOOKUP "SomeSub" PROCESS_BASE SKIP_MARKS DIRECTION LTR\n' 816 "IN_CONTEXT\n" 817 "END_CONTEXT\n" 818 "AS_SUBSTITUTION\n" 819 'SUB GLYPH "A"\n' 820 'WITH GLYPH "A.c2sc"\n' 821 "END_SUB\n" 822 "END_SUBSTITUTION" 823 ).statements 824 self.assertEqual((lookup.name, lookup.process_marks), ("SomeSub", False)) 825 826 def test_substitution_mark_attachment(self): 827 [group, lookup] = self.parse( 828 'DEF_GROUP "SomeMarks"\n' 829 ' ENUM GLYPH "acutecmb" GLYPH "gravecmb" END_ENUM\n' 830 "END_GROUP\n" 831 'DEF_LOOKUP "SomeSub" PROCESS_BASE ' 832 'PROCESS_MARKS "SomeMarks" DIRECTION RTL\n' 833 "IN_CONTEXT\n" 834 "END_CONTEXT\n" 835 "AS_SUBSTITUTION\n" 836 'SUB GLYPH "A"\n' 837 'WITH GLYPH "A.c2sc"\n' 838 "END_SUB\n" 839 "END_SUBSTITUTION" 840 ).statements 841 self.assertEqual((lookup.name, lookup.process_marks), ("SomeSub", "SomeMarks")) 842 843 def test_substitution_mark_glyph_set(self): 844 [group, lookup] = self.parse( 845 'DEF_GROUP "SomeMarks"\n' 846 ' ENUM GLYPH "acutecmb" GLYPH "gravecmb" END_ENUM\n' 847 "END_GROUP\n" 848 'DEF_LOOKUP "SomeSub" PROCESS_BASE ' 849 'PROCESS_MARKS MARK_GLYPH_SET "SomeMarks" DIRECTION RTL\n' 850 "IN_CONTEXT\n" 851 "END_CONTEXT\n" 852 "AS_SUBSTITUTION\n" 853 'SUB GLYPH "A"\n' 854 'WITH GLYPH "A.c2sc"\n' 855 "END_SUB\n" 856 "END_SUBSTITUTION" 857 ).statements 858 self.assertEqual((lookup.name, lookup.mark_glyph_set), ("SomeSub", "SomeMarks")) 859 860 def test_substitution_process_all_marks(self): 861 [group, lookup] = self.parse( 862 'DEF_GROUP "SomeMarks"\n' 863 ' ENUM GLYPH "acutecmb" GLYPH "gravecmb" END_ENUM\n' 864 "END_GROUP\n" 865 'DEF_LOOKUP "SomeSub" PROCESS_BASE PROCESS_MARKS ALL ' 866 "DIRECTION RTL\n" 867 "IN_CONTEXT\n" 868 "END_CONTEXT\n" 869 "AS_SUBSTITUTION\n" 870 'SUB GLYPH "A"\n' 871 'WITH GLYPH "A.c2sc"\n' 872 "END_SUB\n" 873 "END_SUBSTITUTION" 874 ).statements 875 self.assertEqual((lookup.name, lookup.process_marks), ("SomeSub", True)) 876 877 def test_substitution_no_reversal(self): 878 # TODO: check right context with no reversal 879 [lookup] = self.parse( 880 'DEF_LOOKUP "Lookup" PROCESS_BASE PROCESS_MARKS ALL ' 881 "DIRECTION LTR\n" 882 "IN_CONTEXT\n" 883 ' RIGHT ENUM GLYPH "a" GLYPH "b" END_ENUM\n' 884 "END_CONTEXT\n" 885 "AS_SUBSTITUTION\n" 886 'SUB GLYPH "a"\n' 887 'WITH GLYPH "a.alt"\n' 888 "END_SUB\n" 889 "END_SUBSTITUTION" 890 ).statements 891 self.assertEqual((lookup.name, lookup.reversal), ("Lookup", None)) 892 893 def test_substitution_reversal(self): 894 lookup = self.parse( 895 'DEF_GROUP "DFLT_Num_standardFigures"\n' 896 ' ENUM GLYPH "zero" GLYPH "one" GLYPH "two" END_ENUM\n' 897 "END_GROUP\n" 898 'DEF_GROUP "DFLT_Num_numerators"\n' 899 ' ENUM GLYPH "zero.numr" GLYPH "one.numr" GLYPH "two.numr" END_ENUM\n' 900 "END_GROUP\n" 901 'DEF_LOOKUP "RevLookup" PROCESS_BASE PROCESS_MARKS ALL ' 902 "DIRECTION LTR REVERSAL\n" 903 "IN_CONTEXT\n" 904 ' RIGHT ENUM GLYPH "a" GLYPH "b" END_ENUM\n' 905 "END_CONTEXT\n" 906 "AS_SUBSTITUTION\n" 907 'SUB GROUP "DFLT_Num_standardFigures"\n' 908 'WITH GROUP "DFLT_Num_numerators"\n' 909 "END_SUB\n" 910 "END_SUBSTITUTION" 911 ).statements[-1] 912 self.assertEqual((lookup.name, lookup.reversal), ("RevLookup", True)) 913 914 def test_substitution_single_to_multiple(self): 915 [lookup] = self.parse( 916 'DEF_LOOKUP "ccmp" PROCESS_BASE PROCESS_MARKS ALL ' 917 "DIRECTION LTR\n" 918 "IN_CONTEXT\n" 919 "END_CONTEXT\n" 920 "AS_SUBSTITUTION\n" 921 'SUB GLYPH "aacute"\n' 922 'WITH GLYPH "a" GLYPH "acutecomb"\n' 923 "END_SUB\n" 924 'SUB GLYPH "agrave"\n' 925 'WITH GLYPH "a" GLYPH "gravecomb"\n' 926 "END_SUB\n" 927 "END_SUBSTITUTION" 928 ).statements 929 self.assertEqual(lookup.name, "ccmp") 930 self.assertSubEqual( 931 lookup.sub, 932 [["aacute"], ["agrave"]], 933 [["a", "acutecomb"], ["a", "gravecomb"]], 934 ) 935 936 def test_substitution_multiple_to_single(self): 937 [lookup] = self.parse( 938 'DEF_LOOKUP "liga" PROCESS_BASE PROCESS_MARKS ALL ' 939 "DIRECTION LTR\n" 940 "IN_CONTEXT\n" 941 "END_CONTEXT\n" 942 "AS_SUBSTITUTION\n" 943 'SUB GLYPH "f" GLYPH "i"\n' 944 'WITH GLYPH "f_i"\n' 945 "END_SUB\n" 946 'SUB GLYPH "f" GLYPH "t"\n' 947 'WITH GLYPH "f_t"\n' 948 "END_SUB\n" 949 "END_SUBSTITUTION" 950 ).statements 951 self.assertEqual(lookup.name, "liga") 952 self.assertSubEqual(lookup.sub, [["f", "i"], ["f", "t"]], [["f_i"], ["f_t"]]) 953 954 def test_substitution_reverse_chaining_single(self): 955 [lookup] = self.parse( 956 'DEF_LOOKUP "numr" PROCESS_BASE PROCESS_MARKS ALL ' 957 "DIRECTION LTR REVERSAL\n" 958 "IN_CONTEXT\n" 959 " RIGHT ENUM " 960 'GLYPH "fraction" ' 961 'RANGE "zero.numr" TO "nine.numr" ' 962 "END_ENUM\n" 963 "END_CONTEXT\n" 964 "AS_SUBSTITUTION\n" 965 'SUB RANGE "zero" TO "nine"\n' 966 'WITH RANGE "zero.numr" TO "nine.numr"\n' 967 "END_SUB\n" 968 "END_SUBSTITUTION" 969 ).statements 970 971 mapping = lookup.sub.mapping 972 glyphs = [[(r.start, r.end) for r in v] for v in mapping.keys()] 973 replacement = [[(r.start, r.end) for r in v] for v in mapping.values()] 974 975 self.assertEqual(lookup.name, "numr") 976 self.assertEqual(glyphs, [[("zero", "nine")]]) 977 self.assertEqual(replacement, [[("zero.numr", "nine.numr")]]) 978 979 self.assertEqual(len(lookup.context[0].right), 1) 980 self.assertEqual(len(lookup.context[0].right[0]), 1) 981 enum = lookup.context[0].right[0][0] 982 self.assertEqual(len(enum.enum), 2) 983 self.assertEqual(enum.enum[0].glyph, "fraction") 984 self.assertEqual( 985 (enum.enum[1].start, enum.enum[1].end), ("zero.numr", "nine.numr") 986 ) 987 988 # GPOS 989 # ATTACH_CURSIVE 990 # ATTACH 991 # ADJUST_PAIR 992 # ADJUST_SINGLE 993 def test_position_empty(self): 994 with self.assertRaisesRegex( 995 VoltLibError, "Expected ATTACH, ATTACH_CURSIVE, ADJUST_PAIR, ADJUST_SINGLE" 996 ): 997 [lookup] = self.parse( 998 'DEF_LOOKUP "empty_position" PROCESS_BASE PROCESS_MARKS ALL ' 999 "DIRECTION LTR\n" 1000 "EXCEPT_CONTEXT\n" 1001 ' LEFT GLYPH "glyph"\n' 1002 "END_CONTEXT\n" 1003 "AS_POSITION\n" 1004 "END_POSITION" 1005 ).statements 1006 1007 def test_position_attach(self): 1008 [lookup, anchor1, anchor2, anchor3, anchor4] = self.parse( 1009 'DEF_LOOKUP "anchor_top" PROCESS_BASE PROCESS_MARKS ALL ' 1010 "DIRECTION RTL\n" 1011 "IN_CONTEXT\n" 1012 "END_CONTEXT\n" 1013 "AS_POSITION\n" 1014 'ATTACH GLYPH "a" GLYPH "e"\n' 1015 'TO GLYPH "acutecomb" AT ANCHOR "top" ' 1016 'GLYPH "gravecomb" AT ANCHOR "top"\n' 1017 "END_ATTACH\n" 1018 "END_POSITION\n" 1019 'DEF_ANCHOR "MARK_top" ON 120 GLYPH acutecomb COMPONENT 1 ' 1020 "AT POS DX 0 DY 450 END_POS END_ANCHOR\n" 1021 'DEF_ANCHOR "MARK_top" ON 121 GLYPH gravecomb COMPONENT 1 ' 1022 "AT POS DX 0 DY 450 END_POS END_ANCHOR\n" 1023 'DEF_ANCHOR "top" ON 31 GLYPH a COMPONENT 1 ' 1024 "AT POS DX 210 DY 450 END_POS END_ANCHOR\n" 1025 'DEF_ANCHOR "top" ON 35 GLYPH e COMPONENT 1 ' 1026 "AT POS DX 215 DY 450 END_POS END_ANCHOR" 1027 ).statements 1028 pos = lookup.pos 1029 coverage = [g.glyph for g in pos.coverage] 1030 coverage_to = [[[g.glyph for g in e], a] for (e, a) in pos.coverage_to] 1031 self.assertEqual( 1032 (lookup.name, coverage, coverage_to), 1033 ( 1034 "anchor_top", 1035 ["a", "e"], 1036 [[["acutecomb"], "top"], [["gravecomb"], "top"]], 1037 ), 1038 ) 1039 self.assertEqual( 1040 ( 1041 anchor1.name, 1042 anchor1.gid, 1043 anchor1.glyph_name, 1044 anchor1.component, 1045 anchor1.locked, 1046 anchor1.pos, 1047 ), 1048 ("MARK_top", 120, "acutecomb", 1, False, (None, 0, 450, {}, {}, {})), 1049 ) 1050 self.assertEqual( 1051 ( 1052 anchor2.name, 1053 anchor2.gid, 1054 anchor2.glyph_name, 1055 anchor2.component, 1056 anchor2.locked, 1057 anchor2.pos, 1058 ), 1059 ("MARK_top", 121, "gravecomb", 1, False, (None, 0, 450, {}, {}, {})), 1060 ) 1061 self.assertEqual( 1062 ( 1063 anchor3.name, 1064 anchor3.gid, 1065 anchor3.glyph_name, 1066 anchor3.component, 1067 anchor3.locked, 1068 anchor3.pos, 1069 ), 1070 ("top", 31, "a", 1, False, (None, 210, 450, {}, {}, {})), 1071 ) 1072 self.assertEqual( 1073 ( 1074 anchor4.name, 1075 anchor4.gid, 1076 anchor4.glyph_name, 1077 anchor4.component, 1078 anchor4.locked, 1079 anchor4.pos, 1080 ), 1081 ("top", 35, "e", 1, False, (None, 215, 450, {}, {}, {})), 1082 ) 1083 1084 def test_position_attach_cursive(self): 1085 [lookup] = self.parse( 1086 'DEF_LOOKUP "SomeLookup" PROCESS_BASE PROCESS_MARKS ALL ' 1087 "DIRECTION RTL\n" 1088 "IN_CONTEXT\n" 1089 "END_CONTEXT\n" 1090 "AS_POSITION\n" 1091 'ATTACH_CURSIVE\nEXIT GLYPH "a" GLYPH "b"\nENTER GLYPH "c"\n' 1092 "END_ATTACH\n" 1093 "END_POSITION" 1094 ).statements 1095 exit = [[g.glyph for g in v] for v in lookup.pos.coverages_exit] 1096 enter = [[g.glyph for g in v] for v in lookup.pos.coverages_enter] 1097 self.assertEqual( 1098 (lookup.name, exit, enter), ("SomeLookup", [["a", "b"]], [["c"]]) 1099 ) 1100 1101 def test_position_adjust_pair(self): 1102 [lookup] = self.parse( 1103 'DEF_LOOKUP "kern1" PROCESS_BASE PROCESS_MARKS ALL ' 1104 "DIRECTION RTL\n" 1105 "IN_CONTEXT\n" 1106 "END_CONTEXT\n" 1107 "AS_POSITION\n" 1108 "ADJUST_PAIR\n" 1109 ' FIRST GLYPH "A"\n' 1110 ' SECOND GLYPH "V"\n' 1111 " 1 2 BY POS ADV -30 END_POS POS END_POS\n" 1112 " 2 1 BY POS ADV -30 END_POS POS END_POS\n\n" 1113 "END_ADJUST\n" 1114 "END_POSITION" 1115 ).statements 1116 coverages_1 = [[g.glyph for g in v] for v in lookup.pos.coverages_1] 1117 coverages_2 = [[g.glyph for g in v] for v in lookup.pos.coverages_2] 1118 self.assertEqual( 1119 (lookup.name, coverages_1, coverages_2, lookup.pos.adjust_pair), 1120 ( 1121 "kern1", 1122 [["A"]], 1123 [["V"]], 1124 { 1125 (1, 2): ( 1126 (-30, None, None, {}, {}, {}), 1127 (None, None, None, {}, {}, {}), 1128 ), 1129 (2, 1): ( 1130 (-30, None, None, {}, {}, {}), 1131 (None, None, None, {}, {}, {}), 1132 ), 1133 }, 1134 ), 1135 ) 1136 1137 def test_position_adjust_single(self): 1138 [lookup] = self.parse( 1139 'DEF_LOOKUP "TestLookup" PROCESS_BASE PROCESS_MARKS ALL ' 1140 "DIRECTION LTR\n" 1141 "IN_CONTEXT\n" 1142 # ' LEFT GLYPH "leftGlyph"\n' 1143 # ' RIGHT GLYPH "rightGlyph"\n' 1144 "END_CONTEXT\n" 1145 "AS_POSITION\n" 1146 "ADJUST_SINGLE" 1147 ' GLYPH "glyph1" BY POS ADV 0 DX 123 END_POS' 1148 ' GLYPH "glyph2" BY POS ADV 0 DX 456 END_POS\n' 1149 "END_ADJUST\n" 1150 "END_POSITION" 1151 ).statements 1152 pos = lookup.pos 1153 adjust = [[[g.glyph for g in a], b] for (a, b) in pos.adjust_single] 1154 self.assertEqual( 1155 (lookup.name, adjust), 1156 ( 1157 "TestLookup", 1158 [ 1159 [["glyph1"], (0, 123, None, {}, {}, {})], 1160 [["glyph2"], (0, 456, None, {}, {}, {})], 1161 ], 1162 ), 1163 ) 1164 1165 def test_def_anchor(self): 1166 [anchor1, anchor2, anchor3] = self.parse( 1167 'DEF_ANCHOR "top" ON 120 GLYPH a ' 1168 "COMPONENT 1 AT POS DX 250 DY 450 END_POS END_ANCHOR\n" 1169 'DEF_ANCHOR "MARK_top" ON 120 GLYPH acutecomb ' 1170 "COMPONENT 1 AT POS DX 0 DY 450 END_POS END_ANCHOR\n" 1171 'DEF_ANCHOR "bottom" ON 120 GLYPH a ' 1172 "COMPONENT 1 AT POS DX 250 DY 0 END_POS END_ANCHOR" 1173 ).statements 1174 self.assertEqual( 1175 ( 1176 anchor1.name, 1177 anchor1.gid, 1178 anchor1.glyph_name, 1179 anchor1.component, 1180 anchor1.locked, 1181 anchor1.pos, 1182 ), 1183 ("top", 120, "a", 1, False, (None, 250, 450, {}, {}, {})), 1184 ) 1185 self.assertEqual( 1186 ( 1187 anchor2.name, 1188 anchor2.gid, 1189 anchor2.glyph_name, 1190 anchor2.component, 1191 anchor2.locked, 1192 anchor2.pos, 1193 ), 1194 ("MARK_top", 120, "acutecomb", 1, False, (None, 0, 450, {}, {}, {})), 1195 ) 1196 self.assertEqual( 1197 ( 1198 anchor3.name, 1199 anchor3.gid, 1200 anchor3.glyph_name, 1201 anchor3.component, 1202 anchor3.locked, 1203 anchor3.pos, 1204 ), 1205 ("bottom", 120, "a", 1, False, (None, 250, 0, {}, {}, {})), 1206 ) 1207 1208 def test_def_anchor_multi_component(self): 1209 [anchor1, anchor2] = self.parse( 1210 'DEF_ANCHOR "top" ON 120 GLYPH a ' 1211 "COMPONENT 1 AT POS DX 250 DY 450 END_POS END_ANCHOR\n" 1212 'DEF_ANCHOR "top" ON 120 GLYPH a ' 1213 "COMPONENT 2 AT POS DX 250 DY 450 END_POS END_ANCHOR" 1214 ).statements 1215 self.assertEqual( 1216 (anchor1.name, anchor1.gid, anchor1.glyph_name, anchor1.component), 1217 ("top", 120, "a", 1), 1218 ) 1219 self.assertEqual( 1220 (anchor2.name, anchor2.gid, anchor2.glyph_name, anchor2.component), 1221 ("top", 120, "a", 2), 1222 ) 1223 1224 def test_def_anchor_duplicate(self): 1225 self.assertRaisesRegex( 1226 VoltLibError, 1227 'Anchor "dupe" already defined, ' "anchor names are case insensitive", 1228 self.parse, 1229 'DEF_ANCHOR "dupe" ON 120 GLYPH a ' 1230 "COMPONENT 1 AT POS DX 250 DY 450 END_POS END_ANCHOR\n" 1231 'DEF_ANCHOR "dupe" ON 120 GLYPH a ' 1232 "COMPONENT 1 AT POS DX 250 DY 450 END_POS END_ANCHOR", 1233 ) 1234 1235 def test_def_anchor_locked(self): 1236 [anchor] = self.parse( 1237 'DEF_ANCHOR "top" ON 120 GLYPH a ' 1238 "COMPONENT 1 LOCKED AT POS DX 250 DY 450 END_POS END_ANCHOR" 1239 ).statements 1240 self.assertEqual( 1241 ( 1242 anchor.name, 1243 anchor.gid, 1244 anchor.glyph_name, 1245 anchor.component, 1246 anchor.locked, 1247 anchor.pos, 1248 ), 1249 ("top", 120, "a", 1, True, (None, 250, 450, {}, {}, {})), 1250 ) 1251 1252 def test_anchor_adjust_device(self): 1253 [anchor] = self.parse( 1254 'DEF_ANCHOR "MARK_top" ON 123 GLYPH diacglyph ' 1255 "COMPONENT 1 AT POS DX 0 DY 456 ADJUST_BY 12 AT 34 " 1256 "ADJUST_BY 56 AT 78 END_POS END_ANCHOR" 1257 ).statements 1258 self.assertEqual( 1259 (anchor.name, anchor.pos), 1260 ("MARK_top", (None, 0, 456, {}, {}, {34: 12, 78: 56})), 1261 ) 1262 1263 def test_ppem(self): 1264 [grid_ppem, pres_ppem, ppos_ppem] = self.parse( 1265 "GRID_PPEM 20\n" "PRESENTATION_PPEM 72\n" "PPOSITIONING_PPEM 144" 1266 ).statements 1267 self.assertEqual( 1268 ( 1269 (grid_ppem.name, grid_ppem.value), 1270 (pres_ppem.name, pres_ppem.value), 1271 (ppos_ppem.name, ppos_ppem.value), 1272 ), 1273 (("GRID_PPEM", 20), ("PRESENTATION_PPEM", 72), ("PPOSITIONING_PPEM", 144)), 1274 ) 1275 1276 def test_compiler_flags(self): 1277 [setting1, setting2] = self.parse( 1278 "COMPILER_USEEXTENSIONLOOKUPS\n" "COMPILER_USEPAIRPOSFORMAT2" 1279 ).statements 1280 self.assertEqual( 1281 ((setting1.name, setting1.value), (setting2.name, setting2.value)), 1282 ( 1283 ("COMPILER_USEEXTENSIONLOOKUPS", True), 1284 ("COMPILER_USEPAIRPOSFORMAT2", True), 1285 ), 1286 ) 1287 1288 def test_cmap(self): 1289 [cmap_format1, cmap_format2, cmap_format3] = self.parse( 1290 "CMAP_FORMAT 0 3 4\n" "CMAP_FORMAT 1 0 6\n" "CMAP_FORMAT 3 1 4" 1291 ).statements 1292 self.assertEqual( 1293 ( 1294 (cmap_format1.name, cmap_format1.value), 1295 (cmap_format2.name, cmap_format2.value), 1296 (cmap_format3.name, cmap_format3.value), 1297 ), 1298 ( 1299 ("CMAP_FORMAT", (0, 3, 4)), 1300 ("CMAP_FORMAT", (1, 0, 6)), 1301 ("CMAP_FORMAT", (3, 1, 4)), 1302 ), 1303 ) 1304 1305 def test_do_not_touch_cmap(self): 1306 [option1, option2, option3, option4] = self.parse( 1307 "DO_NOT_TOUCH_CMAP\n" 1308 "CMAP_FORMAT 0 3 4\n" 1309 "CMAP_FORMAT 1 0 6\n" 1310 "CMAP_FORMAT 3 1 4" 1311 ).statements 1312 self.assertEqual( 1313 ( 1314 (option1.name, option1.value), 1315 (option2.name, option2.value), 1316 (option3.name, option3.value), 1317 (option4.name, option4.value), 1318 ), 1319 ( 1320 ("DO_NOT_TOUCH_CMAP", True), 1321 ("CMAP_FORMAT", (0, 3, 4)), 1322 ("CMAP_FORMAT", (1, 0, 6)), 1323 ("CMAP_FORMAT", (3, 1, 4)), 1324 ), 1325 ) 1326 1327 def test_stop_at_end(self): 1328 doc = self.parse_('DEF_GLYPH ".notdef" ID 0 TYPE BASE END_GLYPH END\0\0\0\0') 1329 [def_glyph] = doc.statements 1330 self.assertEqual( 1331 ( 1332 def_glyph.name, 1333 def_glyph.id, 1334 def_glyph.unicode, 1335 def_glyph.type, 1336 def_glyph.components, 1337 ), 1338 (".notdef", 0, None, "BASE", None), 1339 ) 1340 self.assertEqual( 1341 str(doc), '\nDEF_GLYPH ".notdef" ID 0 TYPE BASE END_GLYPH END\n' 1342 ) 1343 1344 def parse_(self, text): 1345 return Parser(StringIO(text)).parse() 1346 1347 def parse(self, text): 1348 doc = self.parse_(text) 1349 self.assertEqual("\n".join(str(s) for s in doc.statements), text) 1350 return Parser(StringIO(text)).parse() 1351 1352 1353if __name__ == "__main__": 1354 import sys 1355 1356 sys.exit(unittest.main()) 1357