1# -*- coding: utf-8 -*- 2from fontTools.misc.loggingTools import CapturingLogHandler 3from fontTools.feaLib.error import FeatureLibError 4from fontTools.feaLib.parser import Parser, SymbolTable 5from io import StringIO 6import warnings 7import fontTools.feaLib.ast as ast 8import os 9import unittest 10 11 12def glyphstr(glyphs): 13 def f(x): 14 if len(x) == 1: 15 return list(x)[0] 16 else: 17 return "[%s]" % " ".join(sorted(list(x))) 18 19 return " ".join(f(g.glyphSet()) for g in glyphs) 20 21 22def mapping(s): 23 b = [] 24 for a in s.glyphs: 25 b.extend(a.glyphSet()) 26 c = [] 27 for a in s.replacements: 28 c.extend(a.glyphSet()) 29 if len(c) == 1: 30 c = c * len(b) 31 return dict(zip(b, c)) 32 33 34GLYPHNAMES = ( 35 ( 36 """ 37 .notdef space A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 38 A.sc B.sc C.sc D.sc E.sc F.sc G.sc H.sc I.sc J.sc K.sc L.sc M.sc 39 N.sc O.sc P.sc Q.sc R.sc S.sc T.sc U.sc V.sc W.sc X.sc Y.sc Z.sc 40 A.swash B.swash X.swash Y.swash Z.swash 41 a b c d e f g h i j k l m n o p q r s t u v w x y z 42 a.sc b.sc c.sc d.sc e.sc f.sc g.sc h.sc i.sc j.sc k.sc l.sc m.sc 43 n.sc o.sc p.sc q.sc r.sc s.sc t.sc u.sc v.sc w.sc x.sc y.sc z.sc 44 a.swash b.swash x.swash y.swash z.swash 45 foobar foo.09 foo.1234 foo.9876 46 one two five six acute grave dieresis umlaut cedilla ogonek macron 47 a_f_f_i o_f_f_i f_i f_l f_f_i one.fitted one.oldstyle a.1 a.2 a.3 c_t 48 PRE SUF FIX BACK TRACK LOOK AHEAD ampersand ampersand.1 ampersand.2 49 cid00001 cid00002 cid00003 cid00004 cid00005 cid00006 cid00007 50 cid12345 cid78987 cid00999 cid01000 cid01001 cid00998 cid00995 51 cid00111 cid00222 52 comma endash emdash figuredash damma hamza 53 c_d d.alt n.end s.end f_f 54""" 55 ).split() 56 + ["foo.%d" % i for i in range(1, 200)] 57 + ["G" * 600] 58) 59 60 61class ParserTest(unittest.TestCase): 62 def __init__(self, methodName): 63 unittest.TestCase.__init__(self, methodName) 64 # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, 65 # and fires deprecation warnings if a program uses the old name. 66 if not hasattr(self, "assertRaisesRegex"): 67 self.assertRaisesRegex = self.assertRaisesRegexp 68 69 def test_glyphMap_deprecated(self): 70 glyphMap = {"a": 0, "b": 1, "c": 2} 71 with warnings.catch_warnings(record=True) as w: 72 warnings.simplefilter("always") 73 parser = Parser(StringIO(), glyphMap=glyphMap) 74 75 self.assertEqual(len(w), 1) 76 self.assertEqual(w[-1].category, UserWarning) 77 self.assertIn("deprecated", str(w[-1].message)) 78 self.assertEqual(parser.glyphNames_, {"a", "b", "c"}) 79 80 self.assertRaisesRegex( 81 TypeError, 82 "mutually exclusive", 83 Parser, 84 StringIO(), 85 ("a",), 86 glyphMap={"a": 0}, 87 ) 88 89 self.assertRaisesRegex( 90 TypeError, "unsupported keyword argument", Parser, StringIO(), foo="bar" 91 ) 92 93 def test_comments(self): 94 doc = self.parse( 95 """ # Initial 96 feature test { 97 sub A by B; # simple 98 } test;""" 99 ) 100 c1 = doc.statements[0] 101 c2 = doc.statements[1].statements[1] 102 self.assertEqual(type(c1), ast.Comment) 103 self.assertEqual(c1.text, "# Initial") 104 self.assertEqual(str(c1), "# Initial") 105 self.assertEqual(type(c2), ast.Comment) 106 self.assertEqual(c2.text, "# simple") 107 self.assertEqual(doc.statements[1].name, "test") 108 109 def test_only_comments(self): 110 doc = self.parse( 111 """\ 112 # Initial 113 """ 114 ) 115 c1 = doc.statements[0] 116 self.assertEqual(type(c1), ast.Comment) 117 self.assertEqual(c1.text, "# Initial") 118 self.assertEqual(str(c1), "# Initial") 119 120 def test_anchor_format_a(self): 121 doc = self.parse( 122 "feature test {" 123 " pos cursive A <anchor 120 -20> <anchor NULL>;" 124 "} test;" 125 ) 126 anchor = doc.statements[0].statements[0].entryAnchor 127 self.assertEqual(type(anchor), ast.Anchor) 128 self.assertEqual(anchor.x, 120) 129 self.assertEqual(anchor.y, -20) 130 self.assertIsNone(anchor.contourpoint) 131 self.assertIsNone(anchor.xDeviceTable) 132 self.assertIsNone(anchor.yDeviceTable) 133 134 def test_anchor_format_b(self): 135 doc = self.parse( 136 "feature test {" 137 " pos cursive A <anchor 120 -20 contourpoint 5> <anchor NULL>;" 138 "} test;" 139 ) 140 anchor = doc.statements[0].statements[0].entryAnchor 141 self.assertEqual(type(anchor), ast.Anchor) 142 self.assertEqual(anchor.x, 120) 143 self.assertEqual(anchor.y, -20) 144 self.assertEqual(anchor.contourpoint, 5) 145 self.assertIsNone(anchor.xDeviceTable) 146 self.assertIsNone(anchor.yDeviceTable) 147 148 def test_anchor_format_c(self): 149 doc = self.parse( 150 "feature test {" 151 " pos cursive A " 152 " <anchor 120 -20 <device 11 111, 12 112> <device NULL>>" 153 " <anchor NULL>;" 154 "} test;" 155 ) 156 anchor = doc.statements[0].statements[0].entryAnchor 157 self.assertEqual(type(anchor), ast.Anchor) 158 self.assertEqual(anchor.x, 120) 159 self.assertEqual(anchor.y, -20) 160 self.assertIsNone(anchor.contourpoint) 161 self.assertEqual(anchor.xDeviceTable, ((11, 111), (12, 112))) 162 self.assertIsNone(anchor.yDeviceTable) 163 164 def test_anchor_format_d(self): 165 doc = self.parse( 166 "feature test {" 167 " pos cursive A <anchor 120 -20> <anchor NULL>;" 168 "} test;" 169 ) 170 anchor = doc.statements[0].statements[0].exitAnchor 171 self.assertIsNone(anchor) 172 173 def test_anchor_format_e(self): 174 doc = self.parse( 175 "feature test {" 176 " anchorDef 120 -20 contourpoint 7 Foo;" 177 " pos cursive A <anchor Foo> <anchor NULL>;" 178 "} test;" 179 ) 180 anchor = doc.statements[0].statements[1].entryAnchor 181 self.assertEqual(type(anchor), ast.Anchor) 182 self.assertEqual(anchor.x, 120) 183 self.assertEqual(anchor.y, -20) 184 self.assertEqual(anchor.contourpoint, 7) 185 self.assertIsNone(anchor.xDeviceTable) 186 self.assertIsNone(anchor.yDeviceTable) 187 188 def test_anchor_format_e_undefined(self): 189 self.assertRaisesRegex( 190 FeatureLibError, 191 'Unknown anchor "UnknownName"', 192 self.parse, 193 "feature test {" 194 " position cursive A <anchor UnknownName> <anchor NULL>;" 195 "} test;", 196 ) 197 198 def test_anchor_variable_scalar(self): 199 doc = self.parse( 200 "feature test {" 201 " pos cursive A <anchor (wght=200:-100 wght=900:-150 wdth=150,wght=900:-120) -20> <anchor NULL>;" 202 "} test;" 203 ) 204 anchor = doc.statements[0].statements[0].entryAnchor 205 self.assertEqual( 206 anchor.asFea(), 207 "<anchor (wght=200:-100 wght=900:-150 wdth=150,wght=900:-120) -20>", 208 ) 209 210 def test_anchordef(self): 211 [foo] = self.parse("anchorDef 123 456 foo;").statements 212 self.assertEqual(type(foo), ast.AnchorDefinition) 213 self.assertEqual(foo.name, "foo") 214 self.assertEqual(foo.x, 123) 215 self.assertEqual(foo.y, 456) 216 self.assertEqual(foo.contourpoint, None) 217 218 def test_anchordef_contourpoint(self): 219 [foo] = self.parse("anchorDef 123 456 contourpoint 5 foo;").statements 220 self.assertEqual(type(foo), ast.AnchorDefinition) 221 self.assertEqual(foo.name, "foo") 222 self.assertEqual(foo.x, 123) 223 self.assertEqual(foo.y, 456) 224 self.assertEqual(foo.contourpoint, 5) 225 226 def test_anon(self): 227 anon = self.parse("anon TEST { # a\nfoo\n } TEST; # qux").statements[0] 228 self.assertIsInstance(anon, ast.AnonymousBlock) 229 self.assertEqual(anon.tag, "TEST") 230 self.assertEqual(anon.content, "foo\n ") 231 232 def test_anonymous(self): 233 anon = self.parse("anonymous TEST {\nbar\n} TEST;").statements[0] 234 self.assertIsInstance(anon, ast.AnonymousBlock) 235 self.assertEqual(anon.tag, "TEST") 236 # feature file spec requires passing the final end-of-line 237 self.assertEqual(anon.content, "bar\n") 238 239 def test_anon_missingBrace(self): 240 self.assertRaisesRegex( 241 FeatureLibError, 242 "Expected '} TEST;' to terminate anonymous block", 243 self.parse, 244 "anon TEST { \n no end in sight", 245 ) 246 247 def test_attach(self): 248 doc = self.parse("table GDEF {Attach [a e] 2;} GDEF;") 249 s = doc.statements[0].statements[0] 250 self.assertIsInstance(s, ast.AttachStatement) 251 self.assertEqual(glyphstr([s.glyphs]), "[a e]") 252 self.assertEqual(s.contourPoints, {2}) 253 254 def test_feature_block(self): 255 [liga] = self.parse("feature liga {} liga;").statements 256 self.assertEqual(liga.name, "liga") 257 self.assertFalse(liga.use_extension) 258 259 def test_feature_block_useExtension(self): 260 [liga] = self.parse("feature liga useExtension {} liga;").statements 261 self.assertEqual(liga.name, "liga") 262 self.assertTrue(liga.use_extension) 263 self.assertEqual(liga.asFea(), "feature liga useExtension {\n \n} liga;\n") 264 265 def test_feature_comment(self): 266 [liga] = self.parse("feature liga { # Comment\n } liga;").statements 267 [comment] = liga.statements 268 self.assertIsInstance(comment, ast.Comment) 269 self.assertEqual(comment.text, "# Comment") 270 271 def test_feature_reference(self): 272 doc = self.parse("feature aalt { feature salt; } aalt;") 273 ref = doc.statements[0].statements[0] 274 self.assertIsInstance(ref, ast.FeatureReferenceStatement) 275 self.assertEqual(ref.featureName, "salt") 276 277 def test_FeatureNames_bad(self): 278 self.assertRaisesRegex( 279 FeatureLibError, 280 'Expected "name"', 281 self.parse, 282 "feature ss01 { featureNames { feature test; } ss01;", 283 ) 284 285 def test_FeatureNames_comment(self): 286 [feature] = self.parse( 287 "feature ss01 { featureNames { # Comment\n }; } ss01;" 288 ).statements 289 [featureNames] = feature.statements 290 self.assertIsInstance(featureNames, ast.NestedBlock) 291 [comment] = featureNames.statements 292 self.assertIsInstance(comment, ast.Comment) 293 self.assertEqual(comment.text, "# Comment") 294 295 def test_FeatureNames_emptyStatements(self): 296 [feature] = self.parse( 297 "feature ss01 { featureNames { ;;; }; } ss01;" 298 ).statements 299 [featureNames] = feature.statements 300 self.assertIsInstance(featureNames, ast.NestedBlock) 301 self.assertEqual(featureNames.statements, []) 302 303 def test_FontRevision(self): 304 doc = self.parse("table head {FontRevision 2.5;} head;") 305 s = doc.statements[0].statements[0] 306 self.assertIsInstance(s, ast.FontRevisionStatement) 307 self.assertEqual(s.revision, 2.5) 308 309 def test_FontRevision_negative(self): 310 self.assertRaisesRegex( 311 FeatureLibError, 312 "Font revision numbers must be positive", 313 self.parse, 314 "table head {FontRevision -17.2;} head;", 315 ) 316 317 def test_strict_glyph_name_check(self): 318 self.parse("@bad = [a b ccc];", glyphNames=("a", "b", "ccc")) 319 320 with self.assertRaisesRegex( 321 FeatureLibError, "(?s)missing from the glyph set:.*ccc" 322 ): 323 self.parse("@bad = [a b ccc];", glyphNames=("a", "b")) 324 325 def test_glyphclass(self): 326 [gc] = self.parse("@dash = [endash emdash figuredash];").statements 327 self.assertEqual(gc.name, "dash") 328 self.assertEqual(gc.glyphSet(), ("endash", "emdash", "figuredash")) 329 330 def test_glyphclass_glyphNameTooLong(self): 331 gname = "G" * 600 332 [gc] = self.parse(f"@GlyphClass = [{gname}];").statements 333 self.assertEqual(gc.name, "GlyphClass") 334 self.assertEqual(gc.glyphSet(), (gname,)) 335 336 def test_glyphclass_bad(self): 337 self.assertRaisesRegex( 338 FeatureLibError, 339 "Expected glyph name, glyph range, or glyph class reference", 340 self.parse, 341 "@bad = [a 123];", 342 ) 343 344 def test_glyphclass_duplicate(self): 345 # makeotf accepts this, so we should too 346 ab, xy = self.parse("@dup = [a b]; @dup = [x y];").statements 347 self.assertEqual(glyphstr([ab]), "[a b]") 348 self.assertEqual(glyphstr([xy]), "[x y]") 349 350 def test_glyphclass_empty(self): 351 [gc] = self.parse("@empty_set = [];").statements 352 self.assertEqual(gc.name, "empty_set") 353 self.assertEqual(gc.glyphSet(), tuple()) 354 355 def test_glyphclass_equality(self): 356 [foo, bar] = self.parse("@foo = [a b]; @bar = @foo;").statements 357 self.assertEqual(foo.glyphSet(), ("a", "b")) 358 self.assertEqual(bar.glyphSet(), ("a", "b")) 359 360 def test_glyphclass_from_markClass(self): 361 doc = self.parse( 362 "markClass [acute grave] <anchor 500 800> @TOP_MARKS;" 363 "markClass cedilla <anchor 500 -100> @BOTTOM_MARKS;" 364 "@MARKS = [@TOP_MARKS @BOTTOM_MARKS ogonek];" 365 "@ALL = @MARKS;" 366 ) 367 self.assertEqual( 368 doc.statements[-1].glyphSet(), ("acute", "grave", "cedilla", "ogonek") 369 ) 370 371 def test_glyphclass_range_cid(self): 372 [gc] = self.parse(r"@GlyphClass = [\999-\1001];").statements 373 self.assertEqual(gc.name, "GlyphClass") 374 self.assertEqual(gc.glyphSet(), ("cid00999", "cid01000", "cid01001")) 375 376 def test_glyphclass_range_cid_bad(self): 377 self.assertRaisesRegex( 378 FeatureLibError, 379 "Bad range: start should be less than limit", 380 self.parse, 381 r"@bad = [\998-\995];", 382 ) 383 384 def test_glyphclass_range_uppercase(self): 385 [gc] = self.parse("@swashes = [X.swash-Z.swash];").statements 386 self.assertEqual(gc.name, "swashes") 387 self.assertEqual(gc.glyphSet(), ("X.swash", "Y.swash", "Z.swash")) 388 389 def test_glyphclass_range_lowercase(self): 390 [gc] = self.parse("@defg.sc = [d.sc-g.sc];").statements 391 self.assertEqual(gc.name, "defg.sc") 392 self.assertEqual(gc.glyphSet(), ("d.sc", "e.sc", "f.sc", "g.sc")) 393 394 def test_glyphclass_range_dash(self): 395 glyphNames = "A-foo.sc B-foo.sc C-foo.sc".split() 396 [gc] = self.parse("@range = [A-foo.sc-C-foo.sc];", glyphNames).statements 397 self.assertEqual(gc.glyphSet(), ("A-foo.sc", "B-foo.sc", "C-foo.sc")) 398 399 def test_glyphclass_range_dash_with_space(self): 400 gn = "A-foo.sc B-foo.sc C-foo.sc".split() 401 [gc] = self.parse("@range = [A-foo.sc - C-foo.sc];", gn).statements 402 self.assertEqual(gc.glyphSet(), ("A-foo.sc", "B-foo.sc", "C-foo.sc")) 403 404 def test_glyphclass_ambiguous_dash_no_glyph_names(self): 405 # If Parser is initialized without a glyphNames parameter (or with empty one) 406 # it cannot distinguish between a glyph name containing an hyphen, or a 407 # range of glyph names; thus it will interpret them as literal glyph names 408 # while also outputting a logging warning to alert user about the ambiguity. 409 # https://github.com/fonttools/fonttools/issues/1768 410 glyphNames = () 411 with CapturingLogHandler("fontTools.feaLib.parser", level="WARNING") as caplog: 412 [gc] = self.parse( 413 "@class = [A-foo.sc B-foo.sc C D];", glyphNames 414 ).statements 415 self.assertEqual(gc.glyphSet(), ("A-foo.sc", "B-foo.sc", "C", "D")) 416 self.assertEqual(len(caplog.records), 2) 417 caplog.assertRegex("Ambiguous glyph name that looks like a range:") 418 419 def test_glyphclass_glyph_name_should_win_over_range(self): 420 # The OpenType Feature File Specification v1.20 makes it clear 421 # that if a dashed name could be interpreted either as a glyph name 422 # or as a range, then the semantics should be the single dashed name. 423 glyphNames = "A-foo.sc-C-foo.sc A-foo.sc B-foo.sc C-foo.sc".split() 424 [gc] = self.parse("@range = [A-foo.sc-C-foo.sc];", glyphNames).statements 425 self.assertEqual(gc.glyphSet(), ("A-foo.sc-C-foo.sc",)) 426 427 def test_glyphclass_range_dash_ambiguous(self): 428 glyphNames = "A B C A-B B-C".split() 429 self.assertRaisesRegex( 430 FeatureLibError, 431 'Ambiguous glyph range "A-B-C"; ' 432 'please use "A - B-C" or "A-B - C" to clarify what you mean', 433 self.parse, 434 r"@bad = [A-B-C];", 435 glyphNames, 436 ) 437 438 def test_glyphclass_range_digit1(self): 439 [gc] = self.parse("@range = [foo.2-foo.5];").statements 440 self.assertEqual(gc.glyphSet(), ("foo.2", "foo.3", "foo.4", "foo.5")) 441 442 def test_glyphclass_range_digit2(self): 443 [gc] = self.parse("@range = [foo.09-foo.11];").statements 444 self.assertEqual(gc.glyphSet(), ("foo.09", "foo.10", "foo.11")) 445 446 def test_glyphclass_range_digit3(self): 447 [gc] = self.parse("@range = [foo.123-foo.125];").statements 448 self.assertEqual(gc.glyphSet(), ("foo.123", "foo.124", "foo.125")) 449 450 def test_glyphclass_range_bad(self): 451 self.assertRaisesRegex( 452 FeatureLibError, 453 'Bad range: "a" and "foobar" should have the same length', 454 self.parse, 455 "@bad = [a-foobar];", 456 ) 457 self.assertRaisesRegex( 458 FeatureLibError, 459 'Bad range: "A.swash-z.swash"', 460 self.parse, 461 "@bad = [A.swash-z.swash];", 462 ) 463 self.assertRaisesRegex( 464 FeatureLibError, 465 "Start of range must be smaller than its end", 466 self.parse, 467 "@bad = [B.swash-A.swash];", 468 ) 469 self.assertRaisesRegex( 470 FeatureLibError, 471 'Bad range: "foo.1234-foo.9876"', 472 self.parse, 473 "@bad = [foo.1234-foo.9876];", 474 ) 475 476 def test_glyphclass_range_mixed(self): 477 [gc] = self.parse("@range = [a foo.09-foo.11 X.sc-Z.sc];").statements 478 self.assertEqual( 479 gc.glyphSet(), ("a", "foo.09", "foo.10", "foo.11", "X.sc", "Y.sc", "Z.sc") 480 ) 481 482 def test_glyphclass_reference(self): 483 [vowels_lc, vowels_uc, vowels] = self.parse( 484 "@Vowels.lc = [a e i o u]; @Vowels.uc = [A E I O U];" 485 "@Vowels = [@Vowels.lc @Vowels.uc y Y];" 486 ).statements 487 self.assertEqual(vowels_lc.glyphSet(), tuple("aeiou")) 488 self.assertEqual(vowels_uc.glyphSet(), tuple("AEIOU")) 489 self.assertEqual(vowels.glyphSet(), tuple("aeiouAEIOUyY")) 490 self.assertEqual(vowels.asFea(), "@Vowels = [@Vowels.lc @Vowels.uc y Y];") 491 self.assertRaisesRegex( 492 FeatureLibError, 493 "Unknown glyph class @unknown", 494 self.parse, 495 "@bad = [@unknown];", 496 ) 497 498 def test_glyphclass_scoping(self): 499 [foo, liga, smcp] = self.parse( 500 "@foo = [a b];" 501 "feature liga { @bar = [@foo l]; } liga;" 502 "feature smcp { @bar = [@foo s]; } smcp;" 503 ).statements 504 self.assertEqual(foo.glyphSet(), ("a", "b")) 505 self.assertEqual(liga.statements[0].glyphSet(), ("a", "b", "l")) 506 self.assertEqual(smcp.statements[0].glyphSet(), ("a", "b", "s")) 507 508 def test_glyphclass_scoping_bug496(self): 509 # https://github.com/fonttools/fonttools/issues/496 510 f1, f2 = self.parse( 511 "feature F1 { lookup L { @GLYPHCLASS = [A B C];} L; } F1;" 512 "feature F2 { sub @GLYPHCLASS by D; } F2;" 513 ).statements 514 self.assertEqual(list(f2.statements[0].glyphs[0].glyphSet()), ["A", "B", "C"]) 515 516 def test_GlyphClassDef(self): 517 doc = self.parse("table GDEF {GlyphClassDef [b],[l],[m],[C c];} GDEF;") 518 s = doc.statements[0].statements[0] 519 self.assertIsInstance(s, ast.GlyphClassDefStatement) 520 self.assertEqual(glyphstr([s.baseGlyphs]), "b") 521 self.assertEqual(glyphstr([s.ligatureGlyphs]), "l") 522 self.assertEqual(glyphstr([s.markGlyphs]), "m") 523 self.assertEqual(glyphstr([s.componentGlyphs]), "[C c]") 524 525 def test_GlyphClassDef_noCLassesSpecified(self): 526 doc = self.parse("table GDEF {GlyphClassDef ,,,;} GDEF;") 527 s = doc.statements[0].statements[0] 528 self.assertIsNone(s.baseGlyphs) 529 self.assertIsNone(s.ligatureGlyphs) 530 self.assertIsNone(s.markGlyphs) 531 self.assertIsNone(s.componentGlyphs) 532 533 def test_ignore_pos(self): 534 doc = self.parse("feature test {ignore pos e t' c, q u' u' x;} test;") 535 sub = doc.statements[0].statements[0] 536 self.assertIsInstance(sub, ast.IgnorePosStatement) 537 [(pref1, glyphs1, suff1), (pref2, glyphs2, suff2)] = sub.chainContexts 538 self.assertEqual(glyphstr(pref1), "e") 539 self.assertEqual(glyphstr(glyphs1), "t") 540 self.assertEqual(glyphstr(suff1), "c") 541 self.assertEqual(glyphstr(pref2), "q") 542 self.assertEqual(glyphstr(glyphs2), "u u") 543 self.assertEqual(glyphstr(suff2), "x") 544 545 def test_ignore_position(self): 546 doc = self.parse( 547 "feature test {" " ignore position f [a e] d' [a u]' [e y];" "} test;" 548 ) 549 sub = doc.statements[0].statements[0] 550 self.assertIsInstance(sub, ast.IgnorePosStatement) 551 [(prefix, glyphs, suffix)] = sub.chainContexts 552 self.assertEqual(glyphstr(prefix), "f [a e]") 553 self.assertEqual(glyphstr(glyphs), "d [a u]") 554 self.assertEqual(glyphstr(suffix), "[e y]") 555 556 def test_ignore_position_with_lookup(self): 557 self.assertRaisesRegex( 558 FeatureLibError, 559 'No lookups can be specified for "ignore pos"', 560 self.parse, 561 "lookup L { pos [A A.sc] -100; } L;" 562 "feature test { ignore pos f' i', A' lookup L; } test;", 563 ) 564 565 def test_ignore_sub(self): 566 doc = self.parse("feature test {ignore sub e t' c, q u' u' x;} test;") 567 sub = doc.statements[0].statements[0] 568 self.assertIsInstance(sub, ast.IgnoreSubstStatement) 569 [(pref1, glyphs1, suff1), (pref2, glyphs2, suff2)] = sub.chainContexts 570 self.assertEqual(glyphstr(pref1), "e") 571 self.assertEqual(glyphstr(glyphs1), "t") 572 self.assertEqual(glyphstr(suff1), "c") 573 self.assertEqual(glyphstr(pref2), "q") 574 self.assertEqual(glyphstr(glyphs2), "u u") 575 self.assertEqual(glyphstr(suff2), "x") 576 577 def test_ignore_substitute(self): 578 doc = self.parse( 579 "feature test {" " ignore substitute f [a e] d' [a u]' [e y];" "} test;" 580 ) 581 sub = doc.statements[0].statements[0] 582 self.assertIsInstance(sub, ast.IgnoreSubstStatement) 583 [(prefix, glyphs, suffix)] = sub.chainContexts 584 self.assertEqual(glyphstr(prefix), "f [a e]") 585 self.assertEqual(glyphstr(glyphs), "d [a u]") 586 self.assertEqual(glyphstr(suffix), "[e y]") 587 588 def test_ignore_substitute_with_lookup(self): 589 self.assertRaisesRegex( 590 FeatureLibError, 591 'No lookups can be specified for "ignore sub"', 592 self.parse, 593 "lookup L { sub [A A.sc] by a; } L;" 594 "feature test { ignore sub f' i', A' lookup L; } test;", 595 ) 596 597 def test_include_statement(self): 598 doc = self.parse( 599 """\ 600 include(../family.fea); 601 include # Comment 602 (foo) 603 ; 604 """, 605 followIncludes=False, 606 ) 607 s1, s2, s3 = doc.statements 608 self.assertEqual(type(s1), ast.IncludeStatement) 609 self.assertEqual(s1.filename, "../family.fea") 610 self.assertEqual(s1.asFea(), "include(../family.fea);") 611 self.assertEqual(type(s2), ast.IncludeStatement) 612 self.assertEqual(s2.filename, "foo") 613 self.assertEqual(s2.asFea(), "include(foo);") 614 self.assertEqual(type(s3), ast.Comment) 615 self.assertEqual(s3.text, "# Comment") 616 617 def test_include_statement_no_semicolon(self): 618 doc = self.parse( 619 """\ 620 include(../family.fea) 621 """, 622 followIncludes=False, 623 ) 624 s1 = doc.statements[0] 625 self.assertEqual(type(s1), ast.IncludeStatement) 626 self.assertEqual(s1.filename, "../family.fea") 627 self.assertEqual(s1.asFea(), "include(../family.fea);") 628 629 def test_language(self): 630 doc = self.parse("feature test {language DEU;} test;") 631 s = doc.statements[0].statements[0] 632 self.assertEqual(type(s), ast.LanguageStatement) 633 self.assertEqual(s.language, "DEU ") 634 self.assertTrue(s.include_default) 635 self.assertFalse(s.required) 636 637 def test_language_exclude_dflt(self): 638 doc = self.parse("feature test {language DEU exclude_dflt;} test;") 639 s = doc.statements[0].statements[0] 640 self.assertEqual(type(s), ast.LanguageStatement) 641 self.assertEqual(s.language, "DEU ") 642 self.assertFalse(s.include_default) 643 self.assertFalse(s.required) 644 645 def test_language_exclude_dflt_required(self): 646 doc = self.parse( 647 "feature test {" " language DEU exclude_dflt required;" "} test;" 648 ) 649 s = doc.statements[0].statements[0] 650 self.assertEqual(type(s), ast.LanguageStatement) 651 self.assertEqual(s.language, "DEU ") 652 self.assertFalse(s.include_default) 653 self.assertTrue(s.required) 654 655 def test_language_include_dflt(self): 656 doc = self.parse("feature test {language DEU include_dflt;} test;") 657 s = doc.statements[0].statements[0] 658 self.assertEqual(type(s), ast.LanguageStatement) 659 self.assertEqual(s.language, "DEU ") 660 self.assertTrue(s.include_default) 661 self.assertFalse(s.required) 662 663 def test_language_include_dflt_required(self): 664 doc = self.parse( 665 "feature test {" " language DEU include_dflt required;" "} test;" 666 ) 667 s = doc.statements[0].statements[0] 668 self.assertEqual(type(s), ast.LanguageStatement) 669 self.assertEqual(s.language, "DEU ") 670 self.assertTrue(s.include_default) 671 self.assertTrue(s.required) 672 673 def test_language_DFLT(self): 674 self.assertRaisesRegex( 675 FeatureLibError, 676 '"DFLT" is not a valid language tag; use "dflt" instead', 677 self.parse, 678 "feature test { language DFLT; } test;", 679 ) 680 681 def test_ligatureCaretByIndex_glyphClass(self): 682 doc = self.parse("table GDEF{LigatureCaretByIndex [c_t f_i] 2;}GDEF;") 683 s = doc.statements[0].statements[0] 684 self.assertIsInstance(s, ast.LigatureCaretByIndexStatement) 685 self.assertEqual(glyphstr([s.glyphs]), "[c_t f_i]") 686 self.assertEqual(s.carets, [2]) 687 688 def test_ligatureCaretByIndex_singleGlyph(self): 689 doc = self.parse("table GDEF{LigatureCaretByIndex f_f_i 3 7;}GDEF;") 690 s = doc.statements[0].statements[0] 691 self.assertIsInstance(s, ast.LigatureCaretByIndexStatement) 692 self.assertEqual(glyphstr([s.glyphs]), "f_f_i") 693 self.assertEqual(s.carets, [3, 7]) 694 695 def test_ligatureCaretByPos_glyphClass(self): 696 doc = self.parse("table GDEF {LigatureCaretByPos [c_t f_i] 7;} GDEF;") 697 s = doc.statements[0].statements[0] 698 self.assertIsInstance(s, ast.LigatureCaretByPosStatement) 699 self.assertEqual(glyphstr([s.glyphs]), "[c_t f_i]") 700 self.assertEqual(s.carets, [7]) 701 702 def test_ligatureCaretByPos_singleGlyph(self): 703 doc = self.parse("table GDEF {LigatureCaretByPos f_i 400 380;} GDEF;") 704 s = doc.statements[0].statements[0] 705 self.assertIsInstance(s, ast.LigatureCaretByPosStatement) 706 self.assertEqual(glyphstr([s.glyphs]), "f_i") 707 self.assertEqual(s.carets, [400, 380]) 708 709 def test_ligatureCaretByPos_variable_scalar(self): 710 doc = self.parse( 711 "table GDEF {LigatureCaretByPos f_i (wght=200:400 wght=900:1000) 380;} GDEF;" 712 ) 713 s = doc.statements[0].statements[0] 714 self.assertIsInstance(s, ast.LigatureCaretByPosStatement) 715 self.assertEqual(glyphstr([s.glyphs]), "f_i") 716 self.assertEqual(len(s.carets), 2) 717 self.assertEqual(str(s.carets[0]), "(wght=200:400 wght=900:1000)") 718 self.assertEqual(s.carets[1], 380) 719 720 def test_lookup_block(self): 721 [lookup] = self.parse("lookup Ligatures {} Ligatures;").statements 722 self.assertEqual(lookup.name, "Ligatures") 723 self.assertFalse(lookup.use_extension) 724 725 def test_lookup_block_useExtension(self): 726 [lookup] = self.parse("lookup Foo useExtension {} Foo;").statements 727 self.assertEqual(lookup.name, "Foo") 728 self.assertTrue(lookup.use_extension) 729 self.assertEqual(lookup.asFea(), "lookup Foo useExtension {\n \n} Foo;\n") 730 731 def test_lookup_block_name_mismatch(self): 732 self.assertRaisesRegex( 733 FeatureLibError, 'Expected "Foo"', self.parse, "lookup Foo {} Bar;" 734 ) 735 736 def test_lookup_block_with_horizontal_valueRecordDef(self): 737 doc = self.parse( 738 "feature liga {" 739 " lookup look {" 740 " valueRecordDef 123 foo;" 741 " } look;" 742 "} liga;" 743 ) 744 [liga] = doc.statements 745 [look] = liga.statements 746 [foo] = look.statements 747 self.assertEqual(foo.value.xAdvance, 123) 748 self.assertIsNone(foo.value.yAdvance) 749 750 def test_lookup_block_with_vertical_valueRecordDef(self): 751 doc = self.parse( 752 "feature vkrn {" 753 " lookup look {" 754 " valueRecordDef 123 foo;" 755 " } look;" 756 "} vkrn;" 757 ) 758 [vkrn] = doc.statements 759 [look] = vkrn.statements 760 [foo] = look.statements 761 self.assertIsNone(foo.value.xAdvance) 762 self.assertEqual(foo.value.yAdvance, 123) 763 764 def test_lookup_comment(self): 765 [lookup] = self.parse("lookup L { # Comment\n } L;").statements 766 [comment] = lookup.statements 767 self.assertIsInstance(comment, ast.Comment) 768 self.assertEqual(comment.text, "# Comment") 769 770 def test_lookup_reference(self): 771 [foo, bar] = self.parse( 772 "lookup Foo {} Foo;" "feature Bar {lookup Foo;} Bar;" 773 ).statements 774 [ref] = bar.statements 775 self.assertEqual(type(ref), ast.LookupReferenceStatement) 776 self.assertEqual(ref.lookup, foo) 777 778 def test_lookup_reference_to_lookup_inside_feature(self): 779 [qux, bar] = self.parse( 780 "feature Qux {lookup Foo {} Foo;} Qux;" "feature Bar {lookup Foo;} Bar;" 781 ).statements 782 [foo] = qux.statements 783 [ref] = bar.statements 784 self.assertIsInstance(ref, ast.LookupReferenceStatement) 785 self.assertEqual(ref.lookup, foo) 786 787 def test_lookup_reference_unknown(self): 788 self.assertRaisesRegex( 789 FeatureLibError, 790 'Unknown lookup "Huh"', 791 self.parse, 792 "feature liga {lookup Huh;} liga;", 793 ) 794 795 def parse_lookupflag_(self, s): 796 return self.parse("lookup L {%s} L;" % s).statements[0].statements[-1] 797 798 def test_lookupflag_format_A(self): 799 flag = self.parse_lookupflag_("lookupflag RightToLeft IgnoreMarks;") 800 self.assertIsInstance(flag, ast.LookupFlagStatement) 801 self.assertEqual(flag.value, 9) 802 self.assertIsNone(flag.markAttachment) 803 self.assertIsNone(flag.markFilteringSet) 804 self.assertEqual(flag.asFea(), "lookupflag RightToLeft IgnoreMarks;") 805 806 def test_lookupflag_format_A_MarkAttachmentType(self): 807 flag = self.parse_lookupflag_( 808 "@TOP_MARKS = [acute grave macron];" 809 "lookupflag RightToLeft MarkAttachmentType @TOP_MARKS;" 810 ) 811 self.assertIsInstance(flag, ast.LookupFlagStatement) 812 self.assertEqual(flag.value, 1) 813 self.assertIsInstance(flag.markAttachment, ast.GlyphClassName) 814 self.assertEqual(flag.markAttachment.glyphSet(), ("acute", "grave", "macron")) 815 self.assertIsNone(flag.markFilteringSet) 816 self.assertEqual( 817 flag.asFea(), "lookupflag RightToLeft MarkAttachmentType @TOP_MARKS;" 818 ) 819 820 def test_lookupflag_format_A_MarkAttachmentType_glyphClass(self): 821 flag = self.parse_lookupflag_( 822 "lookupflag RightToLeft MarkAttachmentType [acute grave macron];" 823 ) 824 self.assertIsInstance(flag, ast.LookupFlagStatement) 825 self.assertEqual(flag.value, 1) 826 self.assertIsInstance(flag.markAttachment, ast.GlyphClass) 827 self.assertEqual(flag.markAttachment.glyphSet(), ("acute", "grave", "macron")) 828 self.assertIsNone(flag.markFilteringSet) 829 self.assertEqual( 830 flag.asFea(), 831 "lookupflag RightToLeft MarkAttachmentType [acute grave macron];", 832 ) 833 834 def test_lookupflag_format_A_UseMarkFilteringSet(self): 835 flag = self.parse_lookupflag_( 836 "@BOTTOM_MARKS = [cedilla ogonek];" 837 "lookupflag UseMarkFilteringSet @BOTTOM_MARKS IgnoreLigatures;" 838 ) 839 self.assertIsInstance(flag, ast.LookupFlagStatement) 840 self.assertEqual(flag.value, 4) 841 self.assertIsNone(flag.markAttachment) 842 self.assertIsInstance(flag.markFilteringSet, ast.GlyphClassName) 843 self.assertEqual(flag.markFilteringSet.glyphSet(), ("cedilla", "ogonek")) 844 self.assertEqual( 845 flag.asFea(), 846 "lookupflag IgnoreLigatures UseMarkFilteringSet @BOTTOM_MARKS;", 847 ) 848 849 def test_lookupflag_format_A_UseMarkFilteringSet_glyphClass(self): 850 flag = self.parse_lookupflag_( 851 "lookupflag UseMarkFilteringSet [cedilla ogonek] IgnoreLigatures;" 852 ) 853 self.assertIsInstance(flag, ast.LookupFlagStatement) 854 self.assertEqual(flag.value, 4) 855 self.assertIsNone(flag.markAttachment) 856 self.assertIsInstance(flag.markFilteringSet, ast.GlyphClass) 857 self.assertEqual(flag.markFilteringSet.glyphSet(), ("cedilla", "ogonek")) 858 self.assertEqual( 859 flag.asFea(), 860 "lookupflag IgnoreLigatures UseMarkFilteringSet [cedilla ogonek];", 861 ) 862 863 def test_lookupflag_format_B(self): 864 flag = self.parse_lookupflag_("lookupflag 7;") 865 self.assertIsInstance(flag, ast.LookupFlagStatement) 866 self.assertEqual(flag.value, 7) 867 self.assertIsNone(flag.markAttachment) 868 self.assertIsNone(flag.markFilteringSet) 869 self.assertEqual( 870 flag.asFea(), "lookupflag RightToLeft IgnoreBaseGlyphs IgnoreLigatures;" 871 ) 872 873 def test_lookupflag_format_B_zero(self): 874 flag = self.parse_lookupflag_("lookupflag 0;") 875 self.assertIsInstance(flag, ast.LookupFlagStatement) 876 self.assertEqual(flag.value, 0) 877 self.assertIsNone(flag.markAttachment) 878 self.assertIsNone(flag.markFilteringSet) 879 self.assertEqual(flag.asFea(), "lookupflag 0;") 880 881 def test_lookupflag_no_value(self): 882 self.assertRaisesRegex( 883 FeatureLibError, 884 "lookupflag must have a value", 885 self.parse, 886 "feature test {lookupflag;} test;", 887 ) 888 889 def test_lookupflag_repeated(self): 890 self.assertRaisesRegex( 891 FeatureLibError, 892 "RightToLeft can be specified only once", 893 self.parse, 894 "feature test {lookupflag RightToLeft RightToLeft;} test;", 895 ) 896 897 def test_lookupflag_unrecognized(self): 898 self.assertRaisesRegex( 899 FeatureLibError, 900 '"IgnoreCookies" is not a recognized lookupflag', 901 self.parse, 902 "feature test {lookupflag IgnoreCookies;} test;", 903 ) 904 905 def test_gpos_type_1_glyph(self): 906 doc = self.parse("feature kern {pos one <1 2 3 4>;} kern;") 907 pos = doc.statements[0].statements[0] 908 self.assertIsInstance(pos, ast.SinglePosStatement) 909 [(glyphs, value)] = pos.pos 910 self.assertEqual(glyphstr([glyphs]), "one") 911 self.assertEqual(value.asFea(), "<1 2 3 4>") 912 913 def test_gpos_type_1_glyphclass_horizontal(self): 914 doc = self.parse("feature kern {pos [one two] -300;} kern;") 915 pos = doc.statements[0].statements[0] 916 self.assertIsInstance(pos, ast.SinglePosStatement) 917 [(glyphs, value)] = pos.pos 918 self.assertEqual(glyphstr([glyphs]), "[one two]") 919 self.assertEqual(value.asFea(), "-300") 920 921 def test_gpos_type_1_glyphclass_vertical(self): 922 doc = self.parse("feature vkrn {pos [one two] -300;} vkrn;") 923 pos = doc.statements[0].statements[0] 924 self.assertIsInstance(pos, ast.SinglePosStatement) 925 [(glyphs, value)] = pos.pos 926 self.assertEqual(glyphstr([glyphs]), "[one two]") 927 self.assertEqual(value.asFea(), "-300") 928 929 def test_gpos_type_1_multiple(self): 930 doc = self.parse("feature f {pos one'1 two'2 [five six]'56;} f;") 931 pos = doc.statements[0].statements[0] 932 self.assertIsInstance(pos, ast.SinglePosStatement) 933 [(glyphs1, val1), (glyphs2, val2), (glyphs3, val3)] = pos.pos 934 self.assertEqual(glyphstr([glyphs1]), "one") 935 self.assertEqual(val1.asFea(), "1") 936 self.assertEqual(glyphstr([glyphs2]), "two") 937 self.assertEqual(val2.asFea(), "2") 938 self.assertEqual(glyphstr([glyphs3]), "[five six]") 939 self.assertEqual(val3.asFea(), "56") 940 self.assertEqual(pos.prefix, []) 941 self.assertEqual(pos.suffix, []) 942 943 def test_gpos_type_1_enumerated(self): 944 self.assertRaisesRegex( 945 FeatureLibError, 946 '"enumerate" is only allowed with pair positionings', 947 self.parse, 948 "feature test {enum pos T 100;} test;", 949 ) 950 self.assertRaisesRegex( 951 FeatureLibError, 952 '"enumerate" is only allowed with pair positionings', 953 self.parse, 954 "feature test {enumerate pos T 100;} test;", 955 ) 956 957 def test_gpos_type_1_chained(self): 958 doc = self.parse("feature kern {pos [A B] [T Y]' 20 comma;} kern;") 959 pos = doc.statements[0].statements[0] 960 self.assertIsInstance(pos, ast.SinglePosStatement) 961 [(glyphs, value)] = pos.pos 962 self.assertEqual(glyphstr([glyphs]), "[T Y]") 963 self.assertEqual(value.asFea(), "20") 964 self.assertEqual(glyphstr(pos.prefix), "[A B]") 965 self.assertEqual(glyphstr(pos.suffix), "comma") 966 967 def test_gpos_type_1_chained_special_kern_format_valuerecord_format_a(self): 968 doc = self.parse("feature kern {pos [A B] [T Y]' comma 20;} kern;") 969 pos = doc.statements[0].statements[0] 970 self.assertIsInstance(pos, ast.SinglePosStatement) 971 [(glyphs, value)] = pos.pos 972 self.assertEqual(glyphstr([glyphs]), "[T Y]") 973 self.assertEqual(value.asFea(), "20") 974 self.assertEqual(glyphstr(pos.prefix), "[A B]") 975 self.assertEqual(glyphstr(pos.suffix), "comma") 976 977 def test_gpos_type_1_chained_special_kern_format_valuerecord_format_b(self): 978 doc = self.parse("feature kern {pos [A B] [T Y]' comma <0 0 0 0>;} kern;") 979 pos = doc.statements[0].statements[0] 980 self.assertIsInstance(pos, ast.SinglePosStatement) 981 [(glyphs, value)] = pos.pos 982 self.assertEqual(glyphstr([glyphs]), "[T Y]") 983 self.assertEqual(value.asFea(), "<0 0 0 0>") 984 self.assertEqual(glyphstr(pos.prefix), "[A B]") 985 self.assertEqual(glyphstr(pos.suffix), "comma") 986 987 def test_gpos_type_1_chained_special_kern_format_valuerecord_format_b_bug2293(self): 988 # https://github.com/fonttools/fonttools/issues/2293 989 doc = self.parse("feature kern {pos [A B] [T Y]' comma a <0 0 0 0>;} kern;") 990 pos = doc.statements[0].statements[0] 991 self.assertIsInstance(pos, ast.SinglePosStatement) 992 [(glyphs, value)] = pos.pos 993 self.assertEqual(glyphstr([glyphs]), "[T Y]") 994 self.assertEqual(value.asFea(), "<0 0 0 0>") 995 self.assertEqual(glyphstr(pos.prefix), "[A B]") 996 self.assertEqual(glyphstr(pos.suffix), "comma a") 997 998 def test_gpos_type_1_chained_exception1(self): 999 with self.assertRaisesRegex(FeatureLibError, "Positioning values are allowed"): 1000 doc = self.parse( 1001 "feature kern {" " pos [A B]' [T Y]' comma a <0 0 0 0>;" "} kern;" 1002 ) 1003 1004 def test_gpos_type_1_chained_exception2(self): 1005 with self.assertRaisesRegex(FeatureLibError, "Positioning values are allowed"): 1006 doc = self.parse( 1007 "feature kern {" 1008 " pos [A B]' <0 0 0 0> [T Y]' comma a <0 0 0 0>;" 1009 "} kern;" 1010 ) 1011 1012 def test_gpos_type_1_chained_exception3(self): 1013 with self.assertRaisesRegex(FeatureLibError, "Positioning cannot be applied"): 1014 doc = self.parse( 1015 "feature kern {" 1016 " pos [A B] <0 0 0 0> [T Y]' comma a <0 0 0 0>;" 1017 "} kern;" 1018 ) 1019 1020 def test_gpos_type_1_chained_exception4(self): 1021 with self.assertRaisesRegex(FeatureLibError, "Positioning values are allowed"): 1022 doc = self.parse("feature kern {" " pos a' b c 123 d;" "} kern;") 1023 1024 def test_gpos_type_2_format_a(self): 1025 doc = self.parse( 1026 "feature kern {" " pos [T V] -60 [a b c] <1 2 3 4>;" "} kern;" 1027 ) 1028 pos = doc.statements[0].statements[0] 1029 self.assertEqual(type(pos), ast.PairPosStatement) 1030 self.assertFalse(pos.enumerated) 1031 self.assertEqual(glyphstr([pos.glyphs1]), "[T V]") 1032 self.assertEqual(pos.valuerecord1.asFea(), "-60") 1033 self.assertEqual(glyphstr([pos.glyphs2]), "[a b c]") 1034 self.assertEqual(pos.valuerecord2.asFea(), "<1 2 3 4>") 1035 1036 def test_gpos_type_2_format_a_enumerated(self): 1037 doc = self.parse( 1038 "feature kern {" " enum pos [T V] -60 [a b c] <1 2 3 4>;" "} kern;" 1039 ) 1040 pos = doc.statements[0].statements[0] 1041 self.assertEqual(type(pos), ast.PairPosStatement) 1042 self.assertTrue(pos.enumerated) 1043 self.assertEqual(glyphstr([pos.glyphs1]), "[T V]") 1044 self.assertEqual(pos.valuerecord1.asFea(), "-60") 1045 self.assertEqual(glyphstr([pos.glyphs2]), "[a b c]") 1046 self.assertEqual(pos.valuerecord2.asFea(), "<1 2 3 4>") 1047 1048 def test_gpos_type_2_format_a_with_null_first(self): 1049 doc = self.parse( 1050 "feature kern {" " pos [T V] <NULL> [a b c] <1 2 3 4>;" "} kern;" 1051 ) 1052 pos = doc.statements[0].statements[0] 1053 self.assertEqual(type(pos), ast.PairPosStatement) 1054 self.assertFalse(pos.enumerated) 1055 self.assertEqual(glyphstr([pos.glyphs1]), "[T V]") 1056 self.assertFalse(pos.valuerecord1) 1057 self.assertEqual(pos.valuerecord1.asFea(), "<NULL>") 1058 self.assertEqual(glyphstr([pos.glyphs2]), "[a b c]") 1059 self.assertEqual(pos.valuerecord2.asFea(), "<1 2 3 4>") 1060 self.assertEqual(pos.asFea(), "pos [T V] <NULL> [a b c] <1 2 3 4>;") 1061 1062 def test_gpos_type_2_format_a_with_null_second(self): 1063 doc = self.parse( 1064 "feature kern {" " pos [T V] <1 2 3 4> [a b c] <NULL>;" "} kern;" 1065 ) 1066 pos = doc.statements[0].statements[0] 1067 self.assertEqual(type(pos), ast.PairPosStatement) 1068 self.assertFalse(pos.enumerated) 1069 self.assertEqual(glyphstr([pos.glyphs1]), "[T V]") 1070 self.assertEqual(pos.valuerecord1.asFea(), "<1 2 3 4>") 1071 self.assertEqual(glyphstr([pos.glyphs2]), "[a b c]") 1072 self.assertFalse(pos.valuerecord2) 1073 self.assertEqual(pos.asFea(), "pos [T V] [a b c] <1 2 3 4>;") 1074 1075 def test_gpos_type_2_format_b(self): 1076 doc = self.parse("feature kern {" " pos [T V] [a b c] <1 2 3 4>;" "} kern;") 1077 pos = doc.statements[0].statements[0] 1078 self.assertEqual(type(pos), ast.PairPosStatement) 1079 self.assertFalse(pos.enumerated) 1080 self.assertEqual(glyphstr([pos.glyphs1]), "[T V]") 1081 self.assertEqual(pos.valuerecord1.asFea(), "<1 2 3 4>") 1082 self.assertEqual(glyphstr([pos.glyphs2]), "[a b c]") 1083 self.assertIsNone(pos.valuerecord2) 1084 1085 def test_gpos_type_2_format_b_enumerated(self): 1086 doc = self.parse( 1087 "feature kern {" " enumerate position [T V] [a b c] <1 2 3 4>;" "} kern;" 1088 ) 1089 pos = doc.statements[0].statements[0] 1090 self.assertEqual(type(pos), ast.PairPosStatement) 1091 self.assertTrue(pos.enumerated) 1092 self.assertEqual(glyphstr([pos.glyphs1]), "[T V]") 1093 self.assertEqual(pos.valuerecord1.asFea(), "<1 2 3 4>") 1094 self.assertEqual(glyphstr([pos.glyphs2]), "[a b c]") 1095 self.assertIsNone(pos.valuerecord2) 1096 1097 def test_gpos_type_3(self): 1098 doc = self.parse( 1099 "feature kern {" 1100 " position cursive A <anchor 12 -2> <anchor 2 3>;" 1101 "} kern;" 1102 ) 1103 pos = doc.statements[0].statements[0] 1104 self.assertEqual(type(pos), ast.CursivePosStatement) 1105 self.assertEqual(pos.glyphclass.glyphSet(), ("A",)) 1106 self.assertEqual((pos.entryAnchor.x, pos.entryAnchor.y), (12, -2)) 1107 self.assertEqual((pos.exitAnchor.x, pos.exitAnchor.y), (2, 3)) 1108 1109 def test_gpos_type_3_enumerated(self): 1110 self.assertRaisesRegex( 1111 FeatureLibError, 1112 '"enumerate" is not allowed with cursive attachment positioning', 1113 self.parse, 1114 "feature kern {" 1115 " enumerate position cursive A <anchor 12 -2> <anchor 2 3>;" 1116 "} kern;", 1117 ) 1118 1119 def test_gpos_type_4(self): 1120 doc = self.parse( 1121 "markClass [acute grave] <anchor 150 -10> @TOP_MARKS;" 1122 "markClass [dieresis umlaut] <anchor 300 -10> @TOP_MARKS;" 1123 "markClass [cedilla] <anchor 300 600> @BOTTOM_MARKS;" 1124 "feature test {" 1125 " position base [a e o u] " 1126 " <anchor 250 450> mark @TOP_MARKS " 1127 " <anchor 210 -10> mark @BOTTOM_MARKS;" 1128 "} test;" 1129 ) 1130 pos = doc.statements[-1].statements[0] 1131 self.assertEqual(type(pos), ast.MarkBasePosStatement) 1132 self.assertEqual(pos.base.glyphSet(), ("a", "e", "o", "u")) 1133 (a1, m1), (a2, m2) = pos.marks 1134 self.assertEqual((a1.x, a1.y, m1.name), (250, 450, "TOP_MARKS")) 1135 self.assertEqual((a2.x, a2.y, m2.name), (210, -10, "BOTTOM_MARKS")) 1136 1137 def test_gpos_type_4_enumerated(self): 1138 self.assertRaisesRegex( 1139 FeatureLibError, 1140 '"enumerate" is not allowed with ' "mark-to-base attachment positioning", 1141 self.parse, 1142 "feature kern {" 1143 " markClass cedilla <anchor 300 600> @BOTTOM_MARKS;" 1144 " enumerate position base A <anchor 12 -2> mark @BOTTOM_MARKS;" 1145 "} kern;", 1146 ) 1147 1148 def test_gpos_type_4_not_markClass(self): 1149 self.assertRaisesRegex( 1150 FeatureLibError, 1151 "@MARKS is not a markClass", 1152 self.parse, 1153 "@MARKS = [acute grave];" 1154 "feature test {" 1155 " position base [a e o u] <anchor 250 450> mark @MARKS;" 1156 "} test;", 1157 ) 1158 1159 def test_gpos_type_5(self): 1160 doc = self.parse( 1161 "markClass [grave acute] <anchor 150 500> @TOP_MARKS;" 1162 "markClass [cedilla] <anchor 300 -100> @BOTTOM_MARKS;" 1163 "feature test {" 1164 " position " 1165 " ligature [a_f_f_i o_f_f_i] " 1166 " <anchor 50 600> mark @TOP_MARKS " 1167 " <anchor 50 -10> mark @BOTTOM_MARKS " 1168 " ligComponent " 1169 " <anchor 30 800> mark @TOP_MARKS " 1170 " ligComponent " 1171 " <anchor NULL> " 1172 " ligComponent " 1173 " <anchor 30 -10> mark @BOTTOM_MARKS;" 1174 "} test;" 1175 ) 1176 pos = doc.statements[-1].statements[0] 1177 self.assertEqual(type(pos), ast.MarkLigPosStatement) 1178 self.assertEqual(pos.ligatures.glyphSet(), ("a_f_f_i", "o_f_f_i")) 1179 [(a11, m11), (a12, m12)], [(a2, m2)], [], [(a4, m4)] = pos.marks 1180 self.assertEqual((a11.x, a11.y, m11.name), (50, 600, "TOP_MARKS")) 1181 self.assertEqual((a12.x, a12.y, m12.name), (50, -10, "BOTTOM_MARKS")) 1182 self.assertEqual((a2.x, a2.y, m2.name), (30, 800, "TOP_MARKS")) 1183 self.assertEqual((a4.x, a4.y, m4.name), (30, -10, "BOTTOM_MARKS")) 1184 1185 def test_gpos_type_5_enumerated(self): 1186 self.assertRaisesRegex( 1187 FeatureLibError, 1188 '"enumerate" is not allowed with ' 1189 "mark-to-ligature attachment positioning", 1190 self.parse, 1191 "feature test {" 1192 " markClass cedilla <anchor 300 600> @MARKS;" 1193 " enumerate position " 1194 " ligature f_i <anchor 100 0> mark @MARKS" 1195 " ligComponent <anchor NULL>;" 1196 "} test;", 1197 ) 1198 1199 def test_gpos_type_5_not_markClass(self): 1200 self.assertRaisesRegex( 1201 FeatureLibError, 1202 "@MARKS is not a markClass", 1203 self.parse, 1204 "@MARKS = [acute grave];" 1205 "feature test {" 1206 " position ligature f_i <anchor 250 450> mark @MARKS;" 1207 "} test;", 1208 ) 1209 1210 def test_gpos_type_6(self): 1211 doc = self.parse( 1212 "markClass damma <anchor 189 -103> @MARK_CLASS_1;" 1213 "feature test {" 1214 " position mark hamza <anchor 221 301> mark @MARK_CLASS_1;" 1215 "} test;" 1216 ) 1217 pos = doc.statements[-1].statements[0] 1218 self.assertEqual(type(pos), ast.MarkMarkPosStatement) 1219 self.assertEqual(pos.baseMarks.glyphSet(), ("hamza",)) 1220 [(a1, m1)] = pos.marks 1221 self.assertEqual((a1.x, a1.y, m1.name), (221, 301, "MARK_CLASS_1")) 1222 1223 def test_gpos_type_6_enumerated(self): 1224 self.assertRaisesRegex( 1225 FeatureLibError, 1226 '"enumerate" is not allowed with ' "mark-to-mark attachment positioning", 1227 self.parse, 1228 "markClass damma <anchor 189 -103> @MARK_CLASS_1;" 1229 "feature test {" 1230 " enum pos mark hamza <anchor 221 301> mark @MARK_CLASS_1;" 1231 "} test;", 1232 ) 1233 1234 def test_gpos_type_6_not_markClass(self): 1235 self.assertRaisesRegex( 1236 FeatureLibError, 1237 "@MARKS is not a markClass", 1238 self.parse, 1239 "@MARKS = [acute grave];" 1240 "feature test {" 1241 " position mark cedilla <anchor 250 450> mark @MARKS;" 1242 "} test;", 1243 ) 1244 1245 def test_gpos_type_8(self): 1246 doc = self.parse( 1247 "lookup L1 {pos one 100;} L1; lookup L2 {pos two 200;} L2;" 1248 "feature test {" 1249 " pos [A a] [B b] I' lookup L1 [N n]' lookup L2 P' [Y y] [Z z];" 1250 "} test;" 1251 ) 1252 lookup1, lookup2 = doc.statements[0:2] 1253 pos = doc.statements[-1].statements[0] 1254 self.assertEqual(type(pos), ast.ChainContextPosStatement) 1255 self.assertEqual(glyphstr(pos.prefix), "[A a] [B b]") 1256 self.assertEqual(glyphstr(pos.glyphs), "I [N n] P") 1257 self.assertEqual(glyphstr(pos.suffix), "[Y y] [Z z]") 1258 self.assertEqual(pos.lookups, [[lookup1], [lookup2], None]) 1259 1260 def test_gpos_type_8_lookup_with_values(self): 1261 self.assertRaisesRegex( 1262 FeatureLibError, 1263 'If "lookup" is present, no values must be specified', 1264 self.parse, 1265 "lookup L1 {pos one 100;} L1;" 1266 "feature test {" 1267 " pos A' lookup L1 B' 20;" 1268 "} test;", 1269 ) 1270 1271 def test_markClass(self): 1272 doc = self.parse("markClass [acute grave] <anchor 350 3> @MARKS;") 1273 mc = doc.statements[0] 1274 self.assertIsInstance(mc, ast.MarkClassDefinition) 1275 self.assertEqual(mc.markClass.name, "MARKS") 1276 self.assertEqual(mc.glyphSet(), ("acute", "grave")) 1277 self.assertEqual((mc.anchor.x, mc.anchor.y), (350, 3)) 1278 1279 def test_nameid_windows_utf16(self): 1280 doc = self.parse(r'table name { nameid 9 "M\00fcller-Lanc\00e9"; } name;') 1281 name = doc.statements[0].statements[0] 1282 self.assertIsInstance(name, ast.NameRecord) 1283 self.assertEqual(name.nameID, 9) 1284 self.assertEqual(name.platformID, 3) 1285 self.assertEqual(name.platEncID, 1) 1286 self.assertEqual(name.langID, 0x0409) 1287 self.assertEqual(name.string, "Müller-Lancé") 1288 self.assertEqual(name.asFea(), r'nameid 9 "M\00fcller-Lanc\00e9";') 1289 1290 def test_nameid_windows_utf16_backslash(self): 1291 doc = self.parse(r'table name { nameid 9 "Back\005cslash"; } name;') 1292 name = doc.statements[0].statements[0] 1293 self.assertEqual(name.string, r"Back\slash") 1294 self.assertEqual(name.asFea(), r'nameid 9 "Back\005cslash";') 1295 1296 def test_nameid_windows_utf16_quotation_mark(self): 1297 doc = self.parse(r'table name { nameid 9 "Quotation \0022Mark\0022"; } name;') 1298 name = doc.statements[0].statements[0] 1299 self.assertEqual(name.string, 'Quotation "Mark"') 1300 self.assertEqual(name.asFea(), r'nameid 9 "Quotation \0022Mark\0022";') 1301 1302 def test_nameid_windows_utf16_surroates(self): 1303 doc = self.parse(r'table name { nameid 9 "Carrot \D83E\DD55"; } name;') 1304 name = doc.statements[0].statements[0] 1305 self.assertEqual(name.string, r"Carrot ") 1306 self.assertEqual(name.asFea(), r'nameid 9 "Carrot \d83e\dd55";') 1307 1308 def test_nameid_mac_roman(self): 1309 doc = self.parse(r'table name { nameid 9 1 "Joachim M\9fller-Lanc\8e"; } name;') 1310 name = doc.statements[0].statements[0] 1311 self.assertIsInstance(name, ast.NameRecord) 1312 self.assertEqual(name.nameID, 9) 1313 self.assertEqual(name.platformID, 1) 1314 self.assertEqual(name.platEncID, 0) 1315 self.assertEqual(name.langID, 0) 1316 self.assertEqual(name.string, "Joachim Müller-Lancé") 1317 self.assertEqual(name.asFea(), r'nameid 9 1 "Joachim M\9fller-Lanc\8e";') 1318 1319 def test_nameid_mac_croatian(self): 1320 doc = self.parse(r'table name { nameid 9 1 0 18 "Jovica Veljovi\e6"; } name;') 1321 name = doc.statements[0].statements[0] 1322 self.assertEqual(name.nameID, 9) 1323 self.assertEqual(name.platformID, 1) 1324 self.assertEqual(name.platEncID, 0) 1325 self.assertEqual(name.langID, 18) 1326 self.assertEqual(name.string, "Jovica Veljović") 1327 self.assertEqual(name.asFea(), r'nameid 9 1 0 18 "Jovica Veljovi\e6";') 1328 1329 def test_nameid_unsupported_platform(self): 1330 self.assertRaisesRegex( 1331 FeatureLibError, 1332 "Expected platform id 1 or 3", 1333 self.parse, 1334 'table name { nameid 9 666 "Foo"; } name;', 1335 ) 1336 1337 def test_nameid_hexadecimal(self): 1338 doc = self.parse(r'table name { nameid 0x9 0x3 0x1 0x0409 "Test"; } name;') 1339 name = doc.statements[0].statements[0] 1340 self.assertEqual(name.nameID, 9) 1341 self.assertEqual(name.platformID, 3) 1342 self.assertEqual(name.platEncID, 1) 1343 self.assertEqual(name.langID, 0x0409) 1344 1345 def test_nameid_octal(self): 1346 doc = self.parse(r'table name { nameid 011 03 012 02011 "Test"; } name;') 1347 name = doc.statements[0].statements[0] 1348 self.assertEqual(name.nameID, 9) 1349 self.assertEqual(name.platformID, 3) 1350 self.assertEqual(name.platEncID, 10) 1351 self.assertEqual(name.langID, 0o2011) 1352 1353 def test_cv_hexadecimal(self): 1354 doc = self.parse(r"feature cv01 { cvParameters { Character 0x5DDE; }; } cv01;") 1355 cv = doc.statements[0].statements[0].statements[0] 1356 self.assertEqual(cv.character, 0x5DDE) 1357 1358 def test_cv_octal(self): 1359 doc = self.parse(r"feature cv01 { cvParameters { Character 056736; }; } cv01;") 1360 cv = doc.statements[0].statements[0].statements[0] 1361 self.assertEqual(cv.character, 0o56736) 1362 1363 def test_rsub_format_a(self): 1364 doc = self.parse("feature test {rsub a [b B] c' d [e E] by C;} test;") 1365 rsub = doc.statements[0].statements[0] 1366 self.assertEqual(type(rsub), ast.ReverseChainSingleSubstStatement) 1367 self.assertEqual(glyphstr(rsub.old_prefix), "a [B b]") 1368 self.assertEqual(rsub.glyphs[0].glyphSet(), ("c",)) 1369 self.assertEqual(rsub.replacements[0].glyphSet(), ("C",)) 1370 self.assertEqual(glyphstr(rsub.old_suffix), "d [E e]") 1371 1372 def test_rsub_format_a_cid(self): 1373 doc = self.parse(r"feature test {rsub \1 [\2 \3] \4' \5 by \6;} test;") 1374 rsub = doc.statements[0].statements[0] 1375 self.assertEqual(type(rsub), ast.ReverseChainSingleSubstStatement) 1376 self.assertEqual(glyphstr(rsub.old_prefix), "cid00001 [cid00002 cid00003]") 1377 self.assertEqual(rsub.glyphs[0].glyphSet(), ("cid00004",)) 1378 self.assertEqual(rsub.replacements[0].glyphSet(), ("cid00006",)) 1379 self.assertEqual(glyphstr(rsub.old_suffix), "cid00005") 1380 1381 def test_rsub_format_b(self): 1382 doc = self.parse( 1383 "feature smcp {" 1384 " reversesub A B [one.fitted one.oldstyle]' C [d D] by one;" 1385 "} smcp;" 1386 ) 1387 rsub = doc.statements[0].statements[0] 1388 self.assertEqual(type(rsub), ast.ReverseChainSingleSubstStatement) 1389 self.assertEqual(glyphstr(rsub.old_prefix), "A B") 1390 self.assertEqual(glyphstr(rsub.old_suffix), "C [D d]") 1391 self.assertEqual(mapping(rsub), {"one.fitted": "one", "one.oldstyle": "one"}) 1392 1393 def test_rsub_format_c(self): 1394 doc = self.parse( 1395 "feature test {" 1396 " reversesub BACK TRACK [a-d]' LOOK AHEAD by [A.sc-D.sc];" 1397 "} test;" 1398 ) 1399 rsub = doc.statements[0].statements[0] 1400 self.assertEqual(type(rsub), ast.ReverseChainSingleSubstStatement) 1401 self.assertEqual(glyphstr(rsub.old_prefix), "BACK TRACK") 1402 self.assertEqual(glyphstr(rsub.old_suffix), "LOOK AHEAD") 1403 self.assertEqual( 1404 mapping(rsub), {"a": "A.sc", "b": "B.sc", "c": "C.sc", "d": "D.sc"} 1405 ) 1406 1407 def test_rsub_from(self): 1408 self.assertRaisesRegex( 1409 FeatureLibError, 1410 'Reverse chaining substitutions do not support "from"', 1411 self.parse, 1412 "feature test {rsub a from [a.1 a.2 a.3];} test;", 1413 ) 1414 1415 def test_rsub_nonsingle(self): 1416 self.assertRaisesRegex( 1417 FeatureLibError, 1418 "In reverse chaining single substitutions, only a single glyph " 1419 "or glyph class can be replaced", 1420 self.parse, 1421 "feature test {rsub c d by c_d;} test;", 1422 ) 1423 1424 def test_rsub_multiple_replacement_glyphs(self): 1425 self.assertRaisesRegex( 1426 FeatureLibError, 1427 "In reverse chaining single substitutions, the replacement " 1428 r'\(after "by"\) must be a single glyph or glyph class', 1429 self.parse, 1430 "feature test {rsub f_i by f i;} test;", 1431 ) 1432 1433 def test_script(self): 1434 doc = self.parse("feature test {script cyrl;} test;") 1435 s = doc.statements[0].statements[0] 1436 self.assertEqual(type(s), ast.ScriptStatement) 1437 self.assertEqual(s.script, "cyrl") 1438 1439 def test_script_dflt(self): 1440 self.assertRaisesRegex( 1441 FeatureLibError, 1442 '"dflt" is not a valid script tag; use "DFLT" instead', 1443 self.parse, 1444 "feature test {script dflt;} test;", 1445 ) 1446 1447 def test_stat_design_axis(self): # STAT DesignAxis 1448 doc = self.parse( 1449 "table STAT { DesignAxis opsz 0 " '{name "Optical Size";}; } STAT;' 1450 ) 1451 da = doc.statements[0].statements[0] 1452 self.assertIsInstance(da, ast.STATDesignAxisStatement) 1453 self.assertEqual(da.tag, "opsz") 1454 self.assertEqual(da.axisOrder, 0) 1455 self.assertEqual(da.names[0].string, "Optical Size") 1456 1457 def test_stat_axis_value_format1(self): # STAT AxisValue 1458 doc = self.parse( 1459 "table STAT { DesignAxis opsz 0 " 1460 '{name "Optical Size";}; ' 1461 'AxisValue {location opsz 8; name "Caption";}; } ' 1462 "STAT;" 1463 ) 1464 avr = doc.statements[0].statements[1] 1465 self.assertIsInstance(avr, ast.STATAxisValueStatement) 1466 self.assertEqual(avr.locations[0].tag, "opsz") 1467 self.assertEqual(avr.locations[0].values[0], 8) 1468 self.assertEqual(avr.names[0].string, "Caption") 1469 1470 def test_stat_axis_value_format2(self): # STAT AxisValue 1471 doc = self.parse( 1472 "table STAT { DesignAxis opsz 0 " 1473 '{name "Optical Size";}; ' 1474 'AxisValue {location opsz 8 6 10; name "Caption";}; } ' 1475 "STAT;" 1476 ) 1477 avr = doc.statements[0].statements[1] 1478 self.assertIsInstance(avr, ast.STATAxisValueStatement) 1479 self.assertEqual(avr.locations[0].tag, "opsz") 1480 self.assertEqual(avr.locations[0].values, [8, 6, 10]) 1481 self.assertEqual(avr.names[0].string, "Caption") 1482 1483 def test_stat_axis_value_format2_bad_range(self): # STAT AxisValue 1484 self.assertRaisesRegex( 1485 FeatureLibError, 1486 "Default value 5 is outside of specified range 6-10.", 1487 self.parse, 1488 "table STAT { DesignAxis opsz 0 " 1489 '{name "Optical Size";}; ' 1490 'AxisValue {location opsz 5 6 10; name "Caption";}; } ' 1491 "STAT;", 1492 ) 1493 1494 def test_stat_axis_value_format4(self): # STAT AxisValue 1495 self.assertRaisesRegex( 1496 FeatureLibError, 1497 "Only one value is allowed in a Format 4 Axis Value Record, but 3 were found.", 1498 self.parse, 1499 "table STAT { " 1500 'DesignAxis opsz 0 {name "Optical Size";}; ' 1501 'DesignAxis wdth 0 {name "Width";}; ' 1502 "AxisValue {" 1503 "location opsz 8 6 10; " 1504 "location wdth 400; " 1505 'name "Caption";}; } ' 1506 "STAT;", 1507 ) 1508 1509 def test_stat_elidedfallbackname(self): # STAT ElidedFallbackName 1510 doc = self.parse( 1511 'table STAT { ElidedFallbackName {name "Roman"; ' 1512 'name 3 1 0x0411 "ローマン"; }; ' 1513 "} STAT;" 1514 ) 1515 nameRecord = doc.statements[0].statements[0] 1516 self.assertIsInstance(nameRecord, ast.ElidedFallbackName) 1517 self.assertEqual(nameRecord.names[0].string, "Roman") 1518 self.assertEqual(nameRecord.names[1].string, "ローマン") 1519 1520 def test_stat_elidedfallbacknameid(self): # STAT ElidedFallbackNameID 1521 doc = self.parse( 1522 'table name { nameid 278 "Roman"; } name; ' 1523 "table STAT { ElidedFallbackNameID 278; " 1524 "} STAT;" 1525 ) 1526 nameRecord = doc.statements[0].statements[0] 1527 self.assertIsInstance(nameRecord, ast.NameRecord) 1528 self.assertEqual(nameRecord.string, "Roman") 1529 1530 def test_sub_single_format_a(self): # GSUB LookupType 1 1531 doc = self.parse("feature smcp {substitute a by a.sc;} smcp;") 1532 sub = doc.statements[0].statements[0] 1533 self.assertIsInstance(sub, ast.SingleSubstStatement) 1534 self.assertEqual(glyphstr(sub.prefix), "") 1535 self.assertEqual(mapping(sub), {"a": "a.sc"}) 1536 self.assertEqual(glyphstr(sub.suffix), "") 1537 1538 def test_sub_single_format_a_chained(self): # chain to GSUB LookupType 1 1539 doc = self.parse("feature test {sub [A a] d' [C] by d.alt;} test;") 1540 sub = doc.statements[0].statements[0] 1541 self.assertIsInstance(sub, ast.SingleSubstStatement) 1542 self.assertEqual(mapping(sub), {"d": "d.alt"}) 1543 self.assertEqual(glyphstr(sub.prefix), "[A a]") 1544 self.assertEqual(glyphstr(sub.suffix), "C") 1545 1546 def test_sub_single_format_a_cid(self): # GSUB LookupType 1 1547 doc = self.parse(r"feature smcp {substitute \12345 by \78987;} smcp;") 1548 sub = doc.statements[0].statements[0] 1549 self.assertIsInstance(sub, ast.SingleSubstStatement) 1550 self.assertEqual(glyphstr(sub.prefix), "") 1551 self.assertEqual(mapping(sub), {"cid12345": "cid78987"}) 1552 self.assertEqual(glyphstr(sub.suffix), "") 1553 1554 def test_sub_single_format_b(self): # GSUB LookupType 1 1555 doc = self.parse( 1556 "feature smcp {" 1557 " substitute [one.fitted one.oldstyle] by one;" 1558 "} smcp;" 1559 ) 1560 sub = doc.statements[0].statements[0] 1561 self.assertIsInstance(sub, ast.SingleSubstStatement) 1562 self.assertEqual(mapping(sub), {"one.fitted": "one", "one.oldstyle": "one"}) 1563 self.assertEqual(glyphstr(sub.prefix), "") 1564 self.assertEqual(glyphstr(sub.suffix), "") 1565 1566 def test_sub_single_format_b_chained(self): # chain to GSUB LookupType 1 1567 doc = self.parse( 1568 "feature smcp {" 1569 " substitute PRE FIX [one.fitted one.oldstyle]' SUF FIX by one;" 1570 "} smcp;" 1571 ) 1572 sub = doc.statements[0].statements[0] 1573 self.assertIsInstance(sub, ast.SingleSubstStatement) 1574 self.assertEqual(mapping(sub), {"one.fitted": "one", "one.oldstyle": "one"}) 1575 self.assertEqual(glyphstr(sub.prefix), "PRE FIX") 1576 self.assertEqual(glyphstr(sub.suffix), "SUF FIX") 1577 1578 def test_sub_single_format_c(self): # GSUB LookupType 1 1579 doc = self.parse( 1580 "feature smcp {" " substitute [a-d] by [A.sc-D.sc];" "} smcp;" 1581 ) 1582 sub = doc.statements[0].statements[0] 1583 self.assertIsInstance(sub, ast.SingleSubstStatement) 1584 self.assertEqual( 1585 mapping(sub), {"a": "A.sc", "b": "B.sc", "c": "C.sc", "d": "D.sc"} 1586 ) 1587 self.assertEqual(glyphstr(sub.prefix), "") 1588 self.assertEqual(glyphstr(sub.suffix), "") 1589 1590 def test_sub_single_format_c_chained(self): # chain to GSUB LookupType 1 1591 doc = self.parse( 1592 "feature smcp {" " substitute [a-d]' X Y [Z z] by [A.sc-D.sc];" "} smcp;" 1593 ) 1594 sub = doc.statements[0].statements[0] 1595 self.assertIsInstance(sub, ast.SingleSubstStatement) 1596 self.assertEqual( 1597 mapping(sub), {"a": "A.sc", "b": "B.sc", "c": "C.sc", "d": "D.sc"} 1598 ) 1599 self.assertEqual(glyphstr(sub.prefix), "") 1600 self.assertEqual(glyphstr(sub.suffix), "X Y [Z z]") 1601 1602 def test_sub_single_format_c_different_num_elements(self): 1603 self.assertRaisesRegex( 1604 FeatureLibError, 1605 'Expected a glyph class with 4 elements after "by", ' 1606 "but found a glyph class with 26 elements", 1607 self.parse, 1608 "feature smcp {sub [a-d] by [A.sc-Z.sc];} smcp;", 1609 ) 1610 1611 def test_sub_with_values(self): 1612 self.assertRaisesRegex( 1613 FeatureLibError, 1614 "Substitution statements cannot contain values", 1615 self.parse, 1616 "feature smcp {sub A' 20 by A.sc;} smcp;", 1617 ) 1618 1619 def test_substitute_multiple(self): # GSUB LookupType 2 1620 doc = self.parse("lookup Look {substitute f_f_i by f f i;} Look;") 1621 sub = doc.statements[0].statements[0] 1622 self.assertIsInstance(sub, ast.MultipleSubstStatement) 1623 self.assertEqual(glyphstr([sub.glyph]), "f_f_i") 1624 self.assertEqual(glyphstr(sub.replacement), "f f i") 1625 1626 def test_substitute_multiple_chained(self): # chain to GSUB LookupType 2 1627 doc = self.parse("lookup L {sub [A-C] f_f_i' [X-Z] by f f i;} L;") 1628 sub = doc.statements[0].statements[0] 1629 self.assertIsInstance(sub, ast.MultipleSubstStatement) 1630 self.assertEqual(glyphstr([sub.glyph]), "f_f_i") 1631 self.assertEqual(glyphstr(sub.replacement), "f f i") 1632 1633 def test_substitute_multiple_force_chained(self): 1634 doc = self.parse("lookup L {sub f_f_i' by f f i;} L;") 1635 sub = doc.statements[0].statements[0] 1636 self.assertIsInstance(sub, ast.MultipleSubstStatement) 1637 self.assertEqual(glyphstr([sub.glyph]), "f_f_i") 1638 self.assertEqual(glyphstr(sub.replacement), "f f i") 1639 self.assertEqual(sub.asFea(), "sub f_f_i' by f f i;") 1640 1641 def test_substitute_multiple_classes(self): 1642 doc = self.parse("lookup Look {substitute [f_i f_l] by [f f] [i l];} Look;") 1643 sub = doc.statements[0].statements[0] 1644 self.assertIsInstance(sub, ast.MultipleSubstStatement) 1645 self.assertEqual(glyphstr([sub.glyph]), "[f_i f_l]") 1646 self.assertEqual(glyphstr(sub.replacement), "[f f] [i l]") 1647 1648 def test_substitute_multiple_classes_mixed(self): 1649 doc = self.parse("lookup Look {substitute [f_i f_l] by f [i l];} Look;") 1650 sub = doc.statements[0].statements[0] 1651 self.assertIsInstance(sub, ast.MultipleSubstStatement) 1652 self.assertEqual(glyphstr([sub.glyph]), "[f_i f_l]") 1653 self.assertEqual(glyphstr(sub.replacement), "f [i l]") 1654 1655 def test_substitute_multiple_classes_mixed_singleton(self): 1656 doc = self.parse("lookup Look {substitute [f_i f_l] by [f] [i l];} Look;") 1657 sub = doc.statements[0].statements[0] 1658 self.assertIsInstance(sub, ast.MultipleSubstStatement) 1659 self.assertEqual(glyphstr([sub.glyph]), "[f_i f_l]") 1660 self.assertEqual(glyphstr(sub.replacement), "f [i l]") 1661 1662 def test_substitute_multiple_classes_mismatch(self): 1663 self.assertRaisesRegex( 1664 FeatureLibError, 1665 'Expected a glyph class with 1 or 3 elements after "by", ' 1666 "but found a glyph class with 2 elements", 1667 self.parse, 1668 "lookup Look {substitute [f_i f_l f_f_i] by [f f_f] [i l i];} Look;", 1669 ) 1670 1671 def test_substitute_multiple_by_mutliple(self): 1672 self.assertRaisesRegex( 1673 FeatureLibError, 1674 "Direct substitution of multiple glyphs by multiple glyphs " 1675 "is not supported", 1676 self.parse, 1677 "lookup MxM {sub a b c by d e f;} MxM;", 1678 ) 1679 1680 def test_split_marked_glyphs_runs(self): 1681 self.assertRaisesRegex( 1682 FeatureLibError, 1683 "Unsupported contextual target sequence", 1684 self.parse, 1685 "feature test{" " ignore pos a' x x A';" "} test;", 1686 ) 1687 self.assertRaisesRegex( 1688 FeatureLibError, 1689 "Unsupported contextual target sequence", 1690 self.parse, 1691 "lookup shift {" 1692 " pos a <0 -10 0 0>;" 1693 " pos A <0 10 0 0>;" 1694 "} shift;" 1695 "feature test {" 1696 " sub a' lookup shift x x A' lookup shift;" 1697 "} test;", 1698 ) 1699 self.assertRaisesRegex( 1700 FeatureLibError, 1701 "Unsupported contextual target sequence", 1702 self.parse, 1703 "feature test {" " ignore sub a' x x A';" "} test;", 1704 ) 1705 self.assertRaisesRegex( 1706 FeatureLibError, 1707 "Unsupported contextual target sequence", 1708 self.parse, 1709 "lookup upper {" 1710 " sub a by A;" 1711 "} upper;" 1712 "lookup lower {" 1713 " sub A by a;" 1714 "} lower;" 1715 "feature test {" 1716 " sub a' lookup upper x x A' lookup lower;" 1717 "} test;", 1718 ) 1719 1720 def test_substitute_mix_single_multiple(self): 1721 doc = self.parse( 1722 "lookup Look {" 1723 " sub f_f by f f;" 1724 " sub f by f;" 1725 " sub f_f_i by f f i;" 1726 " sub [a a.sc] by a;" 1727 " sub [a a.sc] by [b b.sc];" 1728 "} Look;" 1729 ) 1730 statements = doc.statements[0].statements 1731 for sub in statements: 1732 self.assertIsInstance(sub, ast.MultipleSubstStatement) 1733 self.assertEqual(statements[1].glyph, "f") 1734 self.assertEqual(statements[1].replacement, ["f"]) 1735 self.assertEqual(statements[3].glyph, "a") 1736 self.assertEqual(statements[3].replacement, ["a"]) 1737 self.assertEqual(statements[4].glyph, "a.sc") 1738 self.assertEqual(statements[4].replacement, ["a"]) 1739 self.assertEqual(statements[5].glyph, "a") 1740 self.assertEqual(statements[5].replacement, ["b"]) 1741 self.assertEqual(statements[6].glyph, "a.sc") 1742 self.assertEqual(statements[6].replacement, ["b.sc"]) 1743 1744 def test_substitute_from(self): # GSUB LookupType 3 1745 doc = self.parse( 1746 "feature test {" " substitute a from [a.1 a.2 a.3];" "} test;" 1747 ) 1748 sub = doc.statements[0].statements[0] 1749 self.assertIsInstance(sub, ast.AlternateSubstStatement) 1750 self.assertEqual(glyphstr(sub.prefix), "") 1751 self.assertEqual(glyphstr([sub.glyph]), "a") 1752 self.assertEqual(glyphstr(sub.suffix), "") 1753 self.assertEqual(glyphstr([sub.replacement]), "[a.1 a.2 a.3]") 1754 1755 def test_substitute_from_chained(self): # chain to GSUB LookupType 3 1756 doc = self.parse( 1757 "feature test {" " substitute A B a' [Y y] Z from [a.1 a.2 a.3];" "} test;" 1758 ) 1759 sub = doc.statements[0].statements[0] 1760 self.assertIsInstance(sub, ast.AlternateSubstStatement) 1761 self.assertEqual(glyphstr(sub.prefix), "A B") 1762 self.assertEqual(glyphstr([sub.glyph]), "a") 1763 self.assertEqual(glyphstr(sub.suffix), "[Y y] Z") 1764 self.assertEqual(glyphstr([sub.replacement]), "[a.1 a.2 a.3]") 1765 1766 def test_substitute_from_cid(self): # GSUB LookupType 3 1767 doc = self.parse( 1768 r"feature test {" r" substitute \7 from [\111 \222];" r"} test;" 1769 ) 1770 sub = doc.statements[0].statements[0] 1771 self.assertIsInstance(sub, ast.AlternateSubstStatement) 1772 self.assertEqual(glyphstr(sub.prefix), "") 1773 self.assertEqual(glyphstr([sub.glyph]), "cid00007") 1774 self.assertEqual(glyphstr(sub.suffix), "") 1775 self.assertEqual(glyphstr([sub.replacement]), "[cid00111 cid00222]") 1776 1777 def test_substitute_from_glyphclass(self): # GSUB LookupType 3 1778 doc = self.parse( 1779 "feature test {" 1780 " @Ampersands = [ampersand.1 ampersand.2];" 1781 " substitute ampersand from @Ampersands;" 1782 "} test;" 1783 ) 1784 [glyphclass, sub] = doc.statements[0].statements 1785 self.assertIsInstance(sub, ast.AlternateSubstStatement) 1786 self.assertEqual(glyphstr(sub.prefix), "") 1787 self.assertEqual(glyphstr([sub.glyph]), "ampersand") 1788 self.assertEqual(glyphstr(sub.suffix), "") 1789 self.assertEqual(glyphstr([sub.replacement]), "[ampersand.1 ampersand.2]") 1790 1791 def test_substitute_ligature(self): # GSUB LookupType 4 1792 doc = self.parse("feature liga {substitute f f i by f_f_i;} liga;") 1793 sub = doc.statements[0].statements[0] 1794 self.assertIsInstance(sub, ast.LigatureSubstStatement) 1795 self.assertEqual(glyphstr(sub.glyphs), "f f i") 1796 self.assertEqual(sub.replacement, "f_f_i") 1797 self.assertEqual(glyphstr(sub.prefix), "") 1798 self.assertEqual(glyphstr(sub.suffix), "") 1799 1800 def test_substitute_ligature_chained(self): # chain to GSUB LookupType 4 1801 doc = self.parse("feature F {substitute A B f' i' Z by f_i;} F;") 1802 sub = doc.statements[0].statements[0] 1803 self.assertIsInstance(sub, ast.LigatureSubstStatement) 1804 self.assertEqual(glyphstr(sub.glyphs), "f i") 1805 self.assertEqual(sub.replacement, "f_i") 1806 self.assertEqual(glyphstr(sub.prefix), "A B") 1807 self.assertEqual(glyphstr(sub.suffix), "Z") 1808 1809 def test_substitute_lookups(self): # GSUB LookupType 6 1810 doc = Parser(self.getpath("spec5fi1.fea"), GLYPHNAMES).parse() 1811 [_, _, _, langsys, ligs, sub, feature] = doc.statements 1812 self.assertEqual(feature.statements[0].lookups, [[ligs], None, [sub]]) 1813 self.assertEqual(feature.statements[1].lookups, [[ligs], None, [sub]]) 1814 1815 def test_substitute_missing_by(self): 1816 self.assertRaisesRegex( 1817 FeatureLibError, 1818 'Expected "by", "from" or explicit lookup references', 1819 self.parse, 1820 "feature liga {substitute f f i;} liga;", 1821 ) 1822 1823 def test_substitute_invalid_statement(self): 1824 self.assertRaisesRegex( 1825 FeatureLibError, 1826 "Invalid substitution statement", 1827 Parser(self.getpath("GSUB_error.fea"), GLYPHNAMES).parse, 1828 ) 1829 1830 def test_subtable(self): 1831 doc = self.parse("feature test {subtable;} test;") 1832 s = doc.statements[0].statements[0] 1833 self.assertIsInstance(s, ast.SubtableStatement) 1834 1835 def test_table_badEnd(self): 1836 self.assertRaisesRegex( 1837 FeatureLibError, 1838 'Expected "GDEF"', 1839 self.parse, 1840 "table GDEF {LigatureCaretByPos f_i 400;} ABCD;", 1841 ) 1842 1843 def test_table_comment(self): 1844 for table in "BASE GDEF OS/2 head hhea name vhea".split(): 1845 doc = self.parse("table %s { # Comment\n } %s;" % (table, table)) 1846 comment = doc.statements[0].statements[0] 1847 self.assertIsInstance(comment, ast.Comment) 1848 self.assertEqual(comment.text, "# Comment") 1849 1850 def test_table_unsupported(self): 1851 self.assertRaisesRegex( 1852 FeatureLibError, 1853 '"table Foo" is not supported', 1854 self.parse, 1855 "table Foo {LigatureCaretByPos f_i 400;} Foo;", 1856 ) 1857 1858 def test_valuerecord_format_a_horizontal(self): 1859 doc = self.parse("feature liga {valueRecordDef 123 foo;} liga;") 1860 valuedef = doc.statements[0].statements[0] 1861 value = valuedef.value 1862 self.assertIsNone(value.xPlacement) 1863 self.assertIsNone(value.yPlacement) 1864 self.assertEqual(value.xAdvance, 123) 1865 self.assertIsNone(value.yAdvance) 1866 self.assertIsNone(value.xPlaDevice) 1867 self.assertIsNone(value.yPlaDevice) 1868 self.assertIsNone(value.xAdvDevice) 1869 self.assertIsNone(value.yAdvDevice) 1870 self.assertEqual(valuedef.asFea(), "valueRecordDef 123 foo;") 1871 self.assertEqual(value.asFea(), "123") 1872 1873 def test_valuerecord_format_a_vertical(self): 1874 doc = self.parse("feature vkrn {valueRecordDef 123 foo;} vkrn;") 1875 valuedef = doc.statements[0].statements[0] 1876 value = valuedef.value 1877 self.assertIsNone(value.xPlacement) 1878 self.assertIsNone(value.yPlacement) 1879 self.assertIsNone(value.xAdvance) 1880 self.assertEqual(value.yAdvance, 123) 1881 self.assertIsNone(value.xPlaDevice) 1882 self.assertIsNone(value.yPlaDevice) 1883 self.assertIsNone(value.xAdvDevice) 1884 self.assertIsNone(value.yAdvDevice) 1885 self.assertEqual(valuedef.asFea(), "valueRecordDef 123 foo;") 1886 self.assertEqual(value.asFea(), "123") 1887 1888 def test_valuerecord_format_a_zero_horizontal(self): 1889 doc = self.parse("feature liga {valueRecordDef 0 foo;} liga;") 1890 valuedef = doc.statements[0].statements[0] 1891 value = valuedef.value 1892 self.assertIsNone(value.xPlacement) 1893 self.assertIsNone(value.yPlacement) 1894 self.assertEqual(value.xAdvance, 0) 1895 self.assertIsNone(value.yAdvance) 1896 self.assertIsNone(value.xPlaDevice) 1897 self.assertIsNone(value.yPlaDevice) 1898 self.assertIsNone(value.xAdvDevice) 1899 self.assertIsNone(value.yAdvDevice) 1900 self.assertEqual(valuedef.asFea(), "valueRecordDef 0 foo;") 1901 self.assertEqual(value.asFea(), "0") 1902 1903 def test_valuerecord_format_a_zero_vertical(self): 1904 doc = self.parse("feature vkrn {valueRecordDef 0 foo;} vkrn;") 1905 valuedef = doc.statements[0].statements[0] 1906 value = valuedef.value 1907 self.assertIsNone(value.xPlacement) 1908 self.assertIsNone(value.yPlacement) 1909 self.assertIsNone(value.xAdvance) 1910 self.assertEqual(value.yAdvance, 0) 1911 self.assertIsNone(value.xPlaDevice) 1912 self.assertIsNone(value.yPlaDevice) 1913 self.assertIsNone(value.xAdvDevice) 1914 self.assertIsNone(value.yAdvDevice) 1915 self.assertEqual(valuedef.asFea(), "valueRecordDef 0 foo;") 1916 self.assertEqual(value.asFea(), "0") 1917 1918 def test_valuerecord_format_a_vertical_contexts_(self): 1919 for tag in "vkrn vpal vhal valt".split(): 1920 doc = self.parse("feature %s {valueRecordDef 77 foo;} %s;" % (tag, tag)) 1921 value = doc.statements[0].statements[0].value 1922 if value.yAdvance != 77: 1923 self.fail( 1924 msg="feature %s should be a vertical context " 1925 "for ValueRecord format A" % tag 1926 ) 1927 1928 def test_valuerecord_format_b(self): 1929 doc = self.parse("feature liga {valueRecordDef <1 2 3 4> foo;} liga;") 1930 valuedef = doc.statements[0].statements[0] 1931 value = valuedef.value 1932 self.assertEqual(value.xPlacement, 1) 1933 self.assertEqual(value.yPlacement, 2) 1934 self.assertEqual(value.xAdvance, 3) 1935 self.assertEqual(value.yAdvance, 4) 1936 self.assertIsNone(value.xPlaDevice) 1937 self.assertIsNone(value.yPlaDevice) 1938 self.assertIsNone(value.xAdvDevice) 1939 self.assertIsNone(value.yAdvDevice) 1940 self.assertEqual(valuedef.asFea(), "valueRecordDef <1 2 3 4> foo;") 1941 self.assertEqual(value.asFea(), "<1 2 3 4>") 1942 1943 def test_valuerecord_format_b_zero(self): 1944 doc = self.parse("feature liga {valueRecordDef <0 0 0 0> foo;} liga;") 1945 valuedef = doc.statements[0].statements[0] 1946 value = valuedef.value 1947 self.assertEqual(value.xPlacement, 0) 1948 self.assertEqual(value.yPlacement, 0) 1949 self.assertEqual(value.xAdvance, 0) 1950 self.assertEqual(value.yAdvance, 0) 1951 self.assertIsNone(value.xPlaDevice) 1952 self.assertIsNone(value.yPlaDevice) 1953 self.assertIsNone(value.xAdvDevice) 1954 self.assertIsNone(value.yAdvDevice) 1955 self.assertEqual(valuedef.asFea(), "valueRecordDef <0 0 0 0> foo;") 1956 self.assertEqual(value.asFea(), "<0 0 0 0>") 1957 1958 def test_valuerecord_format_c(self): 1959 doc = self.parse( 1960 "feature liga {" 1961 " valueRecordDef <" 1962 " 1 2 3 4" 1963 " <device 8 88>" 1964 " <device 11 111, 12 112>" 1965 " <device NULL>" 1966 " <device 33 -113, 44 -114, 55 115>" 1967 " > foo;" 1968 "} liga;" 1969 ) 1970 value = doc.statements[0].statements[0].value 1971 self.assertEqual(value.xPlacement, 1) 1972 self.assertEqual(value.yPlacement, 2) 1973 self.assertEqual(value.xAdvance, 3) 1974 self.assertEqual(value.yAdvance, 4) 1975 self.assertEqual(value.xPlaDevice, ((8, 88),)) 1976 self.assertEqual(value.yPlaDevice, ((11, 111), (12, 112))) 1977 self.assertIsNone(value.xAdvDevice) 1978 self.assertEqual(value.yAdvDevice, ((33, -113), (44, -114), (55, 115))) 1979 self.assertEqual( 1980 value.asFea(), 1981 "<1 2 3 4 <device 8 88> <device 11 111, 12 112>" 1982 " <device NULL> <device 33 -113, 44 -114, 55 115>>", 1983 ) 1984 1985 def test_valuerecord_format_d(self): 1986 doc = self.parse("feature test {valueRecordDef <NULL> foo;} test;") 1987 value = doc.statements[0].statements[0].value 1988 self.assertFalse(value) 1989 self.assertEqual(value.asFea(), "<NULL>") 1990 1991 def test_valuerecord_variable_scalar(self): 1992 doc = self.parse( 1993 "feature test {valueRecordDef <0 (wght=200:-100 wght=900:-150 wdth=150,wght=900:-120) 0 0> foo;} test;" 1994 ) 1995 value = doc.statements[0].statements[0].value 1996 self.assertEqual( 1997 value.asFea(), 1998 "<0 (wght=200:-100 wght=900:-150 wdth=150,wght=900:-120) 0 0>", 1999 ) 2000 2001 def test_valuerecord_named(self): 2002 doc = self.parse( 2003 "valueRecordDef <1 2 3 4> foo;" 2004 "feature liga {valueRecordDef <foo> bar;} liga;" 2005 ) 2006 value = doc.statements[1].statements[0].value 2007 self.assertEqual(value.xPlacement, 1) 2008 self.assertEqual(value.yPlacement, 2) 2009 self.assertEqual(value.xAdvance, 3) 2010 self.assertEqual(value.yAdvance, 4) 2011 2012 def test_valuerecord_named_unknown(self): 2013 self.assertRaisesRegex( 2014 FeatureLibError, 2015 'Unknown valueRecordDef "unknown"', 2016 self.parse, 2017 "valueRecordDef <unknown> foo;", 2018 ) 2019 2020 def test_valuerecord_scoping(self): 2021 [foo, liga, smcp] = self.parse( 2022 "valueRecordDef 789 foo;" 2023 "feature liga {valueRecordDef <foo> bar;} liga;" 2024 "feature smcp {valueRecordDef <foo> bar;} smcp;" 2025 ).statements 2026 self.assertEqual(foo.value.xAdvance, 789) 2027 self.assertEqual(liga.statements[0].value.xAdvance, 789) 2028 self.assertEqual(smcp.statements[0].value.xAdvance, 789) 2029 2030 def test_valuerecord_device_value_out_of_range(self): 2031 self.assertRaisesRegex( 2032 FeatureLibError, 2033 r"Device value out of valid range \(-128..127\)", 2034 self.parse, 2035 "valueRecordDef <1 2 3 4 <device NULL> <device NULL> " 2036 "<device NULL> <device 11 128>> foo;", 2037 ) 2038 2039 def test_conditionset(self): 2040 doc = self.parse("conditionset heavy { wght 700 900; } heavy;") 2041 value = doc.statements[0] 2042 self.assertEqual(value.conditions["wght"], (700, 900)) 2043 self.assertEqual( 2044 value.asFea(), "conditionset heavy {\n wght 700 900;\n} heavy;\n" 2045 ) 2046 2047 doc = self.parse("conditionset heavy { wght 700 900; opsz 17 18;} heavy;") 2048 value = doc.statements[0] 2049 self.assertEqual(value.conditions["wght"], (700, 900)) 2050 self.assertEqual(value.conditions["opsz"], (17, 18)) 2051 self.assertEqual( 2052 value.asFea(), 2053 "conditionset heavy {\n wght 700 900;\n opsz 17 18;\n} heavy;\n", 2054 ) 2055 2056 def test_conditionset_same_axis(self): 2057 self.assertRaisesRegex( 2058 FeatureLibError, 2059 r"Repeated condition for axis wght", 2060 self.parse, 2061 "conditionset heavy { wght 700 900; wght 100 200; } heavy;", 2062 ) 2063 2064 def test_conditionset_float(self): 2065 doc = self.parse("conditionset heavy { wght 700.0 900.0; } heavy;") 2066 value = doc.statements[0] 2067 self.assertEqual(value.conditions["wght"], (700.0, 900.0)) 2068 self.assertEqual( 2069 value.asFea(), "conditionset heavy {\n wght 700.0 900.0;\n} heavy;\n" 2070 ) 2071 2072 def test_variation(self): 2073 doc = self.parse("variation rvrn heavy { sub a by b; } rvrn;") 2074 value = doc.statements[0] 2075 2076 def test_languagesystem(self): 2077 [langsys] = self.parse("languagesystem latn DEU;").statements 2078 self.assertEqual(langsys.script, "latn") 2079 self.assertEqual(langsys.language, "DEU ") 2080 [langsys] = self.parse("languagesystem DFLT DEU;").statements 2081 self.assertEqual(langsys.script, "DFLT") 2082 self.assertEqual(langsys.language, "DEU ") 2083 self.assertRaisesRegex( 2084 FeatureLibError, 2085 '"dflt" is not a valid script tag; use "DFLT" instead', 2086 self.parse, 2087 "languagesystem dflt dflt;", 2088 ) 2089 self.assertRaisesRegex( 2090 FeatureLibError, 2091 '"DFLT" is not a valid language tag; use "dflt" instead', 2092 self.parse, 2093 "languagesystem latn DFLT;", 2094 ) 2095 self.assertRaisesRegex( 2096 FeatureLibError, "Expected ';'", self.parse, "languagesystem latn DEU" 2097 ) 2098 self.assertRaisesRegex( 2099 FeatureLibError, 2100 "longer than 4 characters", 2101 self.parse, 2102 "languagesystem foobar DEU;", 2103 ) 2104 self.assertRaisesRegex( 2105 FeatureLibError, 2106 "longer than 4 characters", 2107 self.parse, 2108 "languagesystem latn FOOBAR;", 2109 ) 2110 2111 def test_empty_statement_ignored(self): 2112 doc = self.parse("feature test {;} test;") 2113 self.assertFalse(doc.statements[0].statements) 2114 doc = self.parse(";;;") 2115 self.assertFalse(doc.statements) 2116 for table in "BASE GDEF OS/2 head hhea name vhea".split(): 2117 doc = self.parse("table %s { ;;; } %s;" % (table, table)) 2118 self.assertEqual(doc.statements[0].statements, []) 2119 2120 def test_ufo_features_parse_include_dir(self): 2121 fea_path = self.getpath("include/test.ufo/features.fea") 2122 include_dir = os.path.dirname(os.path.dirname(fea_path)) 2123 doc = Parser(fea_path, includeDir=include_dir).parse() 2124 assert len(doc.statements) == 1 and doc.statements[0].text == "# Nothing" 2125 2126 def test_unmarked_ignore_statement(self): 2127 with CapturingLogHandler("fontTools.feaLib.parser", level="WARNING") as caplog: 2128 doc = self.parse("lookup foo { ignore sub A; } foo;") 2129 self.assertEqual(doc.statements[0].statements[0].asFea(), "ignore sub A';") 2130 self.assertEqual(len(caplog.records), 1) 2131 caplog.assertRegex( 2132 'Ambiguous "ignore sub", there should be least one marked glyph' 2133 ) 2134 2135 def parse(self, text, glyphNames=GLYPHNAMES, followIncludes=True): 2136 featurefile = StringIO(text) 2137 p = Parser(featurefile, glyphNames, followIncludes=followIncludes) 2138 return p.parse() 2139 2140 @staticmethod 2141 def getpath(testfile): 2142 path, _ = os.path.split(__file__) 2143 return os.path.join(path, "data", testfile) 2144 2145 2146class SymbolTableTest(unittest.TestCase): 2147 def test_scopes(self): 2148 symtab = SymbolTable() 2149 symtab.define("foo", 23) 2150 self.assertEqual(symtab.resolve("foo"), 23) 2151 symtab.enter_scope() 2152 self.assertEqual(symtab.resolve("foo"), 23) 2153 symtab.define("foo", 42) 2154 self.assertEqual(symtab.resolve("foo"), 42) 2155 symtab.exit_scope() 2156 self.assertEqual(symtab.resolve("foo"), 23) 2157 2158 def test_resolve_undefined(self): 2159 self.assertEqual(SymbolTable().resolve("abc"), None) 2160 2161 2162if __name__ == "__main__": 2163 import sys 2164 2165 sys.exit(unittest.main()) 2166