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