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