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