1from fontTools.feaLib.error import FeatureLibError 2from fontTools.feaLib.lexer import Lexer, IncludingLexer, NonIncludingLexer 3from fontTools.misc.encodingTools import getEncoding 4from fontTools.misc.py23 import bytechr, tobytes, tostr 5import fontTools.feaLib.ast as ast 6import logging 7import os 8import re 9 10 11log = logging.getLogger(__name__) 12 13 14class Parser(object): 15 """Initializes a Parser object. 16 17 Example: 18 19 .. code:: python 20 21 from fontTools.feaLib.parser import Parser 22 parser = Parser(file, font.getReverseGlyphMap()) 23 parsetree = parser.parse() 24 25 Note: the ``glyphNames`` iterable serves a double role to help distinguish 26 glyph names from ranges in the presence of hyphens and to ensure that glyph 27 names referenced in a feature file are actually part of a font's glyph set. 28 If the iterable is left empty, no glyph name in glyph set checking takes 29 place, and all glyph tokens containing hyphens are treated as literal glyph 30 names, not as ranges. (Adding a space around the hyphen can, in any case, 31 help to disambiguate ranges from glyph names containing hyphens.) 32 33 By default, the parser will follow ``include()`` statements in the feature 34 file. To turn this off, pass ``followIncludes=False``. Pass a directory string as 35 ``includeDir`` to explicitly declare a directory to search included feature files 36 in. 37 """ 38 39 extensions = {} 40 ast = ast 41 SS_FEATURE_TAGS = {"ss%02d" % i for i in range(1, 20 + 1)} 42 CV_FEATURE_TAGS = {"cv%02d" % i for i in range(1, 99 + 1)} 43 44 def __init__( 45 self, featurefile, glyphNames=(), followIncludes=True, includeDir=None, **kwargs 46 ): 47 48 if "glyphMap" in kwargs: 49 from fontTools.misc.loggingTools import deprecateArgument 50 51 deprecateArgument("glyphMap", "use 'glyphNames' (iterable) instead") 52 if glyphNames: 53 raise TypeError( 54 "'glyphNames' and (deprecated) 'glyphMap' are " "mutually exclusive" 55 ) 56 glyphNames = kwargs.pop("glyphMap") 57 if kwargs: 58 raise TypeError( 59 "unsupported keyword argument%s: %s" 60 % ("" if len(kwargs) == 1 else "s", ", ".join(repr(k) for k in kwargs)) 61 ) 62 63 self.glyphNames_ = set(glyphNames) 64 self.doc_ = self.ast.FeatureFile() 65 self.anchors_ = SymbolTable() 66 self.glyphclasses_ = SymbolTable() 67 self.lookups_ = SymbolTable() 68 self.valuerecords_ = SymbolTable() 69 self.symbol_tables_ = {self.anchors_, self.valuerecords_} 70 self.next_token_type_, self.next_token_ = (None, None) 71 self.cur_comments_ = [] 72 self.next_token_location_ = None 73 lexerClass = IncludingLexer if followIncludes else NonIncludingLexer 74 self.lexer_ = lexerClass(featurefile, includeDir=includeDir) 75 self.advance_lexer_(comments=True) 76 77 def parse(self): 78 """Parse the file, and return a :class:`fontTools.feaLib.ast.FeatureFile` 79 object representing the root of the abstract syntax tree containing the 80 parsed contents of the file.""" 81 statements = self.doc_.statements 82 while self.next_token_type_ is not None or self.cur_comments_: 83 self.advance_lexer_(comments=True) 84 if self.cur_token_type_ is Lexer.COMMENT: 85 statements.append( 86 self.ast.Comment(self.cur_token_, location=self.cur_token_location_) 87 ) 88 elif self.is_cur_keyword_("include"): 89 statements.append(self.parse_include_()) 90 elif self.cur_token_type_ is Lexer.GLYPHCLASS: 91 statements.append(self.parse_glyphclass_definition_()) 92 elif self.is_cur_keyword_(("anon", "anonymous")): 93 statements.append(self.parse_anonymous_()) 94 elif self.is_cur_keyword_("anchorDef"): 95 statements.append(self.parse_anchordef_()) 96 elif self.is_cur_keyword_("languagesystem"): 97 statements.append(self.parse_languagesystem_()) 98 elif self.is_cur_keyword_("lookup"): 99 statements.append(self.parse_lookup_(vertical=False)) 100 elif self.is_cur_keyword_("markClass"): 101 statements.append(self.parse_markClass_()) 102 elif self.is_cur_keyword_("feature"): 103 statements.append(self.parse_feature_block_()) 104 elif self.is_cur_keyword_("table"): 105 statements.append(self.parse_table_()) 106 elif self.is_cur_keyword_("valueRecordDef"): 107 statements.append(self.parse_valuerecord_definition_(vertical=False)) 108 elif ( 109 self.cur_token_type_ is Lexer.NAME 110 and self.cur_token_ in self.extensions 111 ): 112 statements.append(self.extensions[self.cur_token_](self)) 113 elif self.cur_token_type_ is Lexer.SYMBOL and self.cur_token_ == ";": 114 continue 115 else: 116 raise FeatureLibError( 117 "Expected feature, languagesystem, lookup, markClass, " 118 'table, or glyph class definition, got {} "{}"'.format( 119 self.cur_token_type_, self.cur_token_ 120 ), 121 self.cur_token_location_, 122 ) 123 return self.doc_ 124 125 def parse_anchor_(self): 126 # Parses an anchor in any of the four formats given in the feature 127 # file specification (2.e.vii). 128 self.expect_symbol_("<") 129 self.expect_keyword_("anchor") 130 location = self.cur_token_location_ 131 132 if self.next_token_ == "NULL": # Format D 133 self.expect_keyword_("NULL") 134 self.expect_symbol_(">") 135 return None 136 137 if self.next_token_type_ == Lexer.NAME: # Format E 138 name = self.expect_name_() 139 anchordef = self.anchors_.resolve(name) 140 if anchordef is None: 141 raise FeatureLibError( 142 'Unknown anchor "%s"' % name, self.cur_token_location_ 143 ) 144 self.expect_symbol_(">") 145 return self.ast.Anchor( 146 anchordef.x, 147 anchordef.y, 148 name=name, 149 contourpoint=anchordef.contourpoint, 150 xDeviceTable=None, 151 yDeviceTable=None, 152 location=location, 153 ) 154 155 x, y = self.expect_number_(), self.expect_number_() 156 157 contourpoint = None 158 if self.next_token_ == "contourpoint": # Format B 159 self.expect_keyword_("contourpoint") 160 contourpoint = self.expect_number_() 161 162 if self.next_token_ == "<": # Format C 163 xDeviceTable = self.parse_device_() 164 yDeviceTable = self.parse_device_() 165 else: 166 xDeviceTable, yDeviceTable = None, None 167 168 self.expect_symbol_(">") 169 return self.ast.Anchor( 170 x, 171 y, 172 name=None, 173 contourpoint=contourpoint, 174 xDeviceTable=xDeviceTable, 175 yDeviceTable=yDeviceTable, 176 location=location, 177 ) 178 179 def parse_anchor_marks_(self): 180 # Parses a sequence of ``[<anchor> mark @MARKCLASS]*.`` 181 anchorMarks = [] # [(self.ast.Anchor, markClassName)*] 182 while self.next_token_ == "<": 183 anchor = self.parse_anchor_() 184 if anchor is None and self.next_token_ != "mark": 185 continue # <anchor NULL> without mark, eg. in GPOS type 5 186 self.expect_keyword_("mark") 187 markClass = self.expect_markClass_reference_() 188 anchorMarks.append((anchor, markClass)) 189 return anchorMarks 190 191 def parse_anchordef_(self): 192 # Parses a named anchor definition (`section 2.e.viii <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#2.e.vii>`_). 193 assert self.is_cur_keyword_("anchorDef") 194 location = self.cur_token_location_ 195 x, y = self.expect_number_(), self.expect_number_() 196 contourpoint = None 197 if self.next_token_ == "contourpoint": 198 self.expect_keyword_("contourpoint") 199 contourpoint = self.expect_number_() 200 name = self.expect_name_() 201 self.expect_symbol_(";") 202 anchordef = self.ast.AnchorDefinition( 203 name, x, y, contourpoint=contourpoint, location=location 204 ) 205 self.anchors_.define(name, anchordef) 206 return anchordef 207 208 def parse_anonymous_(self): 209 # Parses an anonymous data block (`section 10 <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#10>`_). 210 assert self.is_cur_keyword_(("anon", "anonymous")) 211 tag = self.expect_tag_() 212 _, content, location = self.lexer_.scan_anonymous_block(tag) 213 self.advance_lexer_() 214 self.expect_symbol_("}") 215 end_tag = self.expect_tag_() 216 assert tag == end_tag, "bad splitting in Lexer.scan_anonymous_block()" 217 self.expect_symbol_(";") 218 return self.ast.AnonymousBlock(tag, content, location=location) 219 220 def parse_attach_(self): 221 # Parses a GDEF Attach statement (`section 9.b <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.b>`_) 222 assert self.is_cur_keyword_("Attach") 223 location = self.cur_token_location_ 224 glyphs = self.parse_glyphclass_(accept_glyphname=True) 225 contourPoints = {self.expect_number_()} 226 while self.next_token_ != ";": 227 contourPoints.add(self.expect_number_()) 228 self.expect_symbol_(";") 229 return self.ast.AttachStatement(glyphs, contourPoints, location=location) 230 231 def parse_enumerate_(self, vertical): 232 # Parse an enumerated pair positioning rule (`section 6.b.ii <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#6.b.ii>`_). 233 assert self.cur_token_ in {"enumerate", "enum"} 234 self.advance_lexer_() 235 return self.parse_position_(enumerated=True, vertical=vertical) 236 237 def parse_GlyphClassDef_(self): 238 # Parses 'GlyphClassDef @BASE, @LIGATURES, @MARKS, @COMPONENTS;' 239 assert self.is_cur_keyword_("GlyphClassDef") 240 location = self.cur_token_location_ 241 if self.next_token_ != ",": 242 baseGlyphs = self.parse_glyphclass_(accept_glyphname=False) 243 else: 244 baseGlyphs = None 245 self.expect_symbol_(",") 246 if self.next_token_ != ",": 247 ligatureGlyphs = self.parse_glyphclass_(accept_glyphname=False) 248 else: 249 ligatureGlyphs = None 250 self.expect_symbol_(",") 251 if self.next_token_ != ",": 252 markGlyphs = self.parse_glyphclass_(accept_glyphname=False) 253 else: 254 markGlyphs = None 255 self.expect_symbol_(",") 256 if self.next_token_ != ";": 257 componentGlyphs = self.parse_glyphclass_(accept_glyphname=False) 258 else: 259 componentGlyphs = None 260 self.expect_symbol_(";") 261 return self.ast.GlyphClassDefStatement( 262 baseGlyphs, markGlyphs, ligatureGlyphs, componentGlyphs, location=location 263 ) 264 265 def parse_glyphclass_definition_(self): 266 # Parses glyph class definitions such as '@UPPERCASE = [A-Z];' 267 location, name = self.cur_token_location_, self.cur_token_ 268 self.expect_symbol_("=") 269 glyphs = self.parse_glyphclass_(accept_glyphname=False) 270 self.expect_symbol_(";") 271 glyphclass = self.ast.GlyphClassDefinition(name, glyphs, location=location) 272 self.glyphclasses_.define(name, glyphclass) 273 return glyphclass 274 275 def split_glyph_range_(self, name, location): 276 # Since v1.20, the OpenType Feature File specification allows 277 # for dashes in glyph names. A sequence like "a-b-c-d" could 278 # therefore mean a single glyph whose name happens to be 279 # "a-b-c-d", or it could mean a range from glyph "a" to glyph 280 # "b-c-d", or a range from glyph "a-b" to glyph "c-d", or a 281 # range from glyph "a-b-c" to glyph "d".Technically, this 282 # example could be resolved because the (pretty complex) 283 # definition of glyph ranges renders most of these splits 284 # invalid. But the specification does not say that a compiler 285 # should try to apply such fancy heuristics. To encourage 286 # unambiguous feature files, we therefore try all possible 287 # splits and reject the feature file if there are multiple 288 # splits possible. It is intentional that we don't just emit a 289 # warning; warnings tend to get ignored. To fix the problem, 290 # font designers can trivially add spaces around the intended 291 # split point, and we emit a compiler error that suggests 292 # how exactly the source should be rewritten to make things 293 # unambiguous. 294 parts = name.split("-") 295 solutions = [] 296 for i in range(len(parts)): 297 start, limit = "-".join(parts[0:i]), "-".join(parts[i:]) 298 if start in self.glyphNames_ and limit in self.glyphNames_: 299 solutions.append((start, limit)) 300 if len(solutions) == 1: 301 start, limit = solutions[0] 302 return start, limit 303 elif len(solutions) == 0: 304 raise FeatureLibError( 305 '"%s" is not a glyph in the font, and it can not be split ' 306 "into a range of known glyphs" % name, 307 location, 308 ) 309 else: 310 ranges = " or ".join(['"%s - %s"' % (s, l) for s, l in solutions]) 311 raise FeatureLibError( 312 'Ambiguous glyph range "%s"; ' 313 "please use %s to clarify what you mean" % (name, ranges), 314 location, 315 ) 316 317 def parse_glyphclass_(self, accept_glyphname, accept_null=False): 318 # Parses a glyph class, either named or anonymous, or (if 319 # ``bool(accept_glyphname)``) a glyph name. If ``bool(accept_null)`` then 320 # also accept the special NULL glyph. 321 if accept_glyphname and self.next_token_type_ in (Lexer.NAME, Lexer.CID): 322 if accept_null and self.next_token_ == "NULL": 323 # If you want a glyph called NULL, you should escape it. 324 self.advance_lexer_() 325 return self.ast.NullGlyph(location=self.cur_token_location_) 326 glyph = self.expect_glyph_() 327 self.check_glyph_name_in_glyph_set(glyph) 328 return self.ast.GlyphName(glyph, location=self.cur_token_location_) 329 if self.next_token_type_ is Lexer.GLYPHCLASS: 330 self.advance_lexer_() 331 gc = self.glyphclasses_.resolve(self.cur_token_) 332 if gc is None: 333 raise FeatureLibError( 334 "Unknown glyph class @%s" % self.cur_token_, 335 self.cur_token_location_, 336 ) 337 if isinstance(gc, self.ast.MarkClass): 338 return self.ast.MarkClassName(gc, location=self.cur_token_location_) 339 else: 340 return self.ast.GlyphClassName(gc, location=self.cur_token_location_) 341 342 self.expect_symbol_("[") 343 location = self.cur_token_location_ 344 glyphs = self.ast.GlyphClass(location=location) 345 while self.next_token_ != "]": 346 if self.next_token_type_ is Lexer.NAME: 347 glyph = self.expect_glyph_() 348 location = self.cur_token_location_ 349 if "-" in glyph and self.glyphNames_ and glyph not in self.glyphNames_: 350 start, limit = self.split_glyph_range_(glyph, location) 351 self.check_glyph_name_in_glyph_set(start, limit) 352 glyphs.add_range( 353 start, limit, self.make_glyph_range_(location, start, limit) 354 ) 355 elif self.next_token_ == "-": 356 start = glyph 357 self.expect_symbol_("-") 358 limit = self.expect_glyph_() 359 self.check_glyph_name_in_glyph_set(start, limit) 360 glyphs.add_range( 361 start, limit, self.make_glyph_range_(location, start, limit) 362 ) 363 else: 364 if "-" in glyph and not self.glyphNames_: 365 log.warning( 366 str( 367 FeatureLibError( 368 f"Ambiguous glyph name that looks like a range: {glyph!r}", 369 location, 370 ) 371 ) 372 ) 373 self.check_glyph_name_in_glyph_set(glyph) 374 glyphs.append(glyph) 375 elif self.next_token_type_ is Lexer.CID: 376 glyph = self.expect_glyph_() 377 if self.next_token_ == "-": 378 range_location = self.cur_token_location_ 379 range_start = self.cur_token_ 380 self.expect_symbol_("-") 381 range_end = self.expect_cid_() 382 self.check_glyph_name_in_glyph_set( 383 f"cid{range_start:05d}", 384 f"cid{range_end:05d}", 385 ) 386 glyphs.add_cid_range( 387 range_start, 388 range_end, 389 self.make_cid_range_(range_location, range_start, range_end), 390 ) 391 else: 392 glyph_name = f"cid{self.cur_token_:05d}" 393 self.check_glyph_name_in_glyph_set(glyph_name) 394 glyphs.append(glyph_name) 395 elif self.next_token_type_ is Lexer.GLYPHCLASS: 396 self.advance_lexer_() 397 gc = self.glyphclasses_.resolve(self.cur_token_) 398 if gc is None: 399 raise FeatureLibError( 400 "Unknown glyph class @%s" % self.cur_token_, 401 self.cur_token_location_, 402 ) 403 if isinstance(gc, self.ast.MarkClass): 404 gc = self.ast.MarkClassName(gc, location=self.cur_token_location_) 405 else: 406 gc = self.ast.GlyphClassName(gc, location=self.cur_token_location_) 407 glyphs.add_class(gc) 408 else: 409 raise FeatureLibError( 410 "Expected glyph name, glyph range, " 411 f"or glyph class reference, found {self.next_token_!r}", 412 self.next_token_location_, 413 ) 414 self.expect_symbol_("]") 415 return glyphs 416 417 def parse_glyph_pattern_(self, vertical): 418 # Parses a glyph pattern, including lookups and context, e.g.:: 419 # 420 # a b 421 # a b c' d e 422 # a b c' lookup ChangeC d e 423 prefix, glyphs, lookups, values, suffix = ([], [], [], [], []) 424 hasMarks = False 425 while self.next_token_ not in {"by", "from", ";", ","}: 426 gc = self.parse_glyphclass_(accept_glyphname=True) 427 marked = False 428 if self.next_token_ == "'": 429 self.expect_symbol_("'") 430 hasMarks = marked = True 431 if marked: 432 if suffix: 433 # makeotf also reports this as an error, while FontForge 434 # silently inserts ' in all the intervening glyphs. 435 # https://github.com/fonttools/fonttools/pull/1096 436 raise FeatureLibError( 437 "Unsupported contextual target sequence: at most " 438 "one run of marked (') glyph/class names allowed", 439 self.cur_token_location_, 440 ) 441 glyphs.append(gc) 442 elif glyphs: 443 suffix.append(gc) 444 else: 445 prefix.append(gc) 446 447 if self.is_next_value_(): 448 values.append(self.parse_valuerecord_(vertical)) 449 else: 450 values.append(None) 451 452 lookuplist = None 453 while self.next_token_ == "lookup": 454 if lookuplist is None: 455 lookuplist = [] 456 self.expect_keyword_("lookup") 457 if not marked: 458 raise FeatureLibError( 459 "Lookups can only follow marked glyphs", 460 self.cur_token_location_, 461 ) 462 lookup_name = self.expect_name_() 463 lookup = self.lookups_.resolve(lookup_name) 464 if lookup is None: 465 raise FeatureLibError( 466 'Unknown lookup "%s"' % lookup_name, self.cur_token_location_ 467 ) 468 lookuplist.append(lookup) 469 if marked: 470 lookups.append(lookuplist) 471 472 if not glyphs and not suffix: # eg., "sub f f i by" 473 assert lookups == [] 474 return ([], prefix, [None] * len(prefix), values, [], hasMarks) 475 else: 476 assert not any(values[: len(prefix)]), values 477 format1 = values[len(prefix) :][: len(glyphs)] 478 format2 = values[(len(prefix) + len(glyphs)) :][: len(suffix)] 479 values = ( 480 format2 481 if format2 and isinstance(format2[0], self.ast.ValueRecord) 482 else format1 483 ) 484 return (prefix, glyphs, lookups, values, suffix, hasMarks) 485 486 def parse_chain_context_(self): 487 location = self.cur_token_location_ 488 prefix, glyphs, lookups, values, suffix, hasMarks = self.parse_glyph_pattern_( 489 vertical=False 490 ) 491 chainContext = [(prefix, glyphs, suffix)] 492 hasLookups = any(lookups) 493 while self.next_token_ == ",": 494 self.expect_symbol_(",") 495 ( 496 prefix, 497 glyphs, 498 lookups, 499 values, 500 suffix, 501 hasMarks, 502 ) = self.parse_glyph_pattern_(vertical=False) 503 chainContext.append((prefix, glyphs, suffix)) 504 hasLookups = hasLookups or any(lookups) 505 self.expect_symbol_(";") 506 return chainContext, hasLookups 507 508 def parse_ignore_(self): 509 # Parses an ignore sub/pos rule. 510 assert self.is_cur_keyword_("ignore") 511 location = self.cur_token_location_ 512 self.advance_lexer_() 513 if self.cur_token_ in ["substitute", "sub"]: 514 chainContext, hasLookups = self.parse_chain_context_() 515 if hasLookups: 516 raise FeatureLibError( 517 'No lookups can be specified for "ignore sub"', location 518 ) 519 return self.ast.IgnoreSubstStatement(chainContext, location=location) 520 if self.cur_token_ in ["position", "pos"]: 521 chainContext, hasLookups = self.parse_chain_context_() 522 if hasLookups: 523 raise FeatureLibError( 524 'No lookups can be specified for "ignore pos"', location 525 ) 526 return self.ast.IgnorePosStatement(chainContext, location=location) 527 raise FeatureLibError( 528 'Expected "substitute" or "position"', self.cur_token_location_ 529 ) 530 531 def parse_include_(self): 532 assert self.cur_token_ == "include" 533 location = self.cur_token_location_ 534 filename = self.expect_filename_() 535 # self.expect_symbol_(";") 536 return ast.IncludeStatement(filename, location=location) 537 538 def parse_language_(self): 539 assert self.is_cur_keyword_("language") 540 location = self.cur_token_location_ 541 language = self.expect_language_tag_() 542 include_default, required = (True, False) 543 if self.next_token_ in {"exclude_dflt", "include_dflt"}: 544 include_default = self.expect_name_() == "include_dflt" 545 if self.next_token_ == "required": 546 self.expect_keyword_("required") 547 required = True 548 self.expect_symbol_(";") 549 return self.ast.LanguageStatement( 550 language, include_default, required, location=location 551 ) 552 553 def parse_ligatureCaretByIndex_(self): 554 assert self.is_cur_keyword_("LigatureCaretByIndex") 555 location = self.cur_token_location_ 556 glyphs = self.parse_glyphclass_(accept_glyphname=True) 557 carets = [self.expect_number_()] 558 while self.next_token_ != ";": 559 carets.append(self.expect_number_()) 560 self.expect_symbol_(";") 561 return self.ast.LigatureCaretByIndexStatement(glyphs, carets, location=location) 562 563 def parse_ligatureCaretByPos_(self): 564 assert self.is_cur_keyword_("LigatureCaretByPos") 565 location = self.cur_token_location_ 566 glyphs = self.parse_glyphclass_(accept_glyphname=True) 567 carets = [self.expect_number_()] 568 while self.next_token_ != ";": 569 carets.append(self.expect_number_()) 570 self.expect_symbol_(";") 571 return self.ast.LigatureCaretByPosStatement(glyphs, carets, location=location) 572 573 def parse_lookup_(self, vertical): 574 # Parses a ``lookup`` - either a lookup block, or a lookup reference 575 # inside a feature. 576 assert self.is_cur_keyword_("lookup") 577 location, name = self.cur_token_location_, self.expect_name_() 578 579 if self.next_token_ == ";": 580 lookup = self.lookups_.resolve(name) 581 if lookup is None: 582 raise FeatureLibError( 583 'Unknown lookup "%s"' % name, self.cur_token_location_ 584 ) 585 self.expect_symbol_(";") 586 return self.ast.LookupReferenceStatement(lookup, location=location) 587 588 use_extension = False 589 if self.next_token_ == "useExtension": 590 self.expect_keyword_("useExtension") 591 use_extension = True 592 593 block = self.ast.LookupBlock(name, use_extension, location=location) 594 self.parse_block_(block, vertical) 595 self.lookups_.define(name, block) 596 return block 597 598 def parse_lookupflag_(self): 599 # Parses a ``lookupflag`` statement, either specified by number or 600 # in words. 601 assert self.is_cur_keyword_("lookupflag") 602 location = self.cur_token_location_ 603 604 # format B: "lookupflag 6;" 605 if self.next_token_type_ == Lexer.NUMBER: 606 value = self.expect_number_() 607 self.expect_symbol_(";") 608 return self.ast.LookupFlagStatement(value, location=location) 609 610 # format A: "lookupflag RightToLeft MarkAttachmentType @M;" 611 value_seen = False 612 value, markAttachment, markFilteringSet = 0, None, None 613 flags = { 614 "RightToLeft": 1, 615 "IgnoreBaseGlyphs": 2, 616 "IgnoreLigatures": 4, 617 "IgnoreMarks": 8, 618 } 619 seen = set() 620 while self.next_token_ != ";": 621 if self.next_token_ in seen: 622 raise FeatureLibError( 623 "%s can be specified only once" % self.next_token_, 624 self.next_token_location_, 625 ) 626 seen.add(self.next_token_) 627 if self.next_token_ == "MarkAttachmentType": 628 self.expect_keyword_("MarkAttachmentType") 629 markAttachment = self.parse_glyphclass_(accept_glyphname=False) 630 elif self.next_token_ == "UseMarkFilteringSet": 631 self.expect_keyword_("UseMarkFilteringSet") 632 markFilteringSet = self.parse_glyphclass_(accept_glyphname=False) 633 elif self.next_token_ in flags: 634 value_seen = True 635 value = value | flags[self.expect_name_()] 636 else: 637 raise FeatureLibError( 638 '"%s" is not a recognized lookupflag' % self.next_token_, 639 self.next_token_location_, 640 ) 641 self.expect_symbol_(";") 642 643 if not any([value_seen, markAttachment, markFilteringSet]): 644 raise FeatureLibError( 645 "lookupflag must have a value", self.next_token_location_ 646 ) 647 648 return self.ast.LookupFlagStatement( 649 value, 650 markAttachment=markAttachment, 651 markFilteringSet=markFilteringSet, 652 location=location, 653 ) 654 655 def parse_markClass_(self): 656 assert self.is_cur_keyword_("markClass") 657 location = self.cur_token_location_ 658 glyphs = self.parse_glyphclass_(accept_glyphname=True) 659 anchor = self.parse_anchor_() 660 name = self.expect_class_name_() 661 self.expect_symbol_(";") 662 markClass = self.doc_.markClasses.get(name) 663 if markClass is None: 664 markClass = self.ast.MarkClass(name) 665 self.doc_.markClasses[name] = markClass 666 self.glyphclasses_.define(name, markClass) 667 mcdef = self.ast.MarkClassDefinition( 668 markClass, anchor, glyphs, location=location 669 ) 670 markClass.addDefinition(mcdef) 671 return mcdef 672 673 def parse_position_(self, enumerated, vertical): 674 assert self.cur_token_ in {"position", "pos"} 675 if self.next_token_ == "cursive": # GPOS type 3 676 return self.parse_position_cursive_(enumerated, vertical) 677 elif self.next_token_ == "base": # GPOS type 4 678 return self.parse_position_base_(enumerated, vertical) 679 elif self.next_token_ == "ligature": # GPOS type 5 680 return self.parse_position_ligature_(enumerated, vertical) 681 elif self.next_token_ == "mark": # GPOS type 6 682 return self.parse_position_mark_(enumerated, vertical) 683 684 location = self.cur_token_location_ 685 prefix, glyphs, lookups, values, suffix, hasMarks = self.parse_glyph_pattern_( 686 vertical 687 ) 688 self.expect_symbol_(";") 689 690 if any(lookups): 691 # GPOS type 8: Chaining contextual positioning; explicit lookups 692 if any(values): 693 raise FeatureLibError( 694 'If "lookup" is present, no values must be specified', location 695 ) 696 return self.ast.ChainContextPosStatement( 697 prefix, glyphs, suffix, lookups, location=location 698 ) 699 700 # Pair positioning, format A: "pos V 10 A -10;" 701 # Pair positioning, format B: "pos V A -20;" 702 if not prefix and not suffix and len(glyphs) == 2 and not hasMarks: 703 if values[0] is None: # Format B: "pos V A -20;" 704 values.reverse() 705 return self.ast.PairPosStatement( 706 glyphs[0], 707 values[0], 708 glyphs[1], 709 values[1], 710 enumerated=enumerated, 711 location=location, 712 ) 713 714 if enumerated: 715 raise FeatureLibError( 716 '"enumerate" is only allowed with pair positionings', location 717 ) 718 return self.ast.SinglePosStatement( 719 list(zip(glyphs, values)), 720 prefix, 721 suffix, 722 forceChain=hasMarks, 723 location=location, 724 ) 725 726 def parse_position_cursive_(self, enumerated, vertical): 727 location = self.cur_token_location_ 728 self.expect_keyword_("cursive") 729 if enumerated: 730 raise FeatureLibError( 731 '"enumerate" is not allowed with ' "cursive attachment positioning", 732 location, 733 ) 734 glyphclass = self.parse_glyphclass_(accept_glyphname=True) 735 entryAnchor = self.parse_anchor_() 736 exitAnchor = self.parse_anchor_() 737 self.expect_symbol_(";") 738 return self.ast.CursivePosStatement( 739 glyphclass, entryAnchor, exitAnchor, location=location 740 ) 741 742 def parse_position_base_(self, enumerated, vertical): 743 location = self.cur_token_location_ 744 self.expect_keyword_("base") 745 if enumerated: 746 raise FeatureLibError( 747 '"enumerate" is not allowed with ' 748 "mark-to-base attachment positioning", 749 location, 750 ) 751 base = self.parse_glyphclass_(accept_glyphname=True) 752 marks = self.parse_anchor_marks_() 753 self.expect_symbol_(";") 754 return self.ast.MarkBasePosStatement(base, marks, location=location) 755 756 def parse_position_ligature_(self, enumerated, vertical): 757 location = self.cur_token_location_ 758 self.expect_keyword_("ligature") 759 if enumerated: 760 raise FeatureLibError( 761 '"enumerate" is not allowed with ' 762 "mark-to-ligature attachment positioning", 763 location, 764 ) 765 ligatures = self.parse_glyphclass_(accept_glyphname=True) 766 marks = [self.parse_anchor_marks_()] 767 while self.next_token_ == "ligComponent": 768 self.expect_keyword_("ligComponent") 769 marks.append(self.parse_anchor_marks_()) 770 self.expect_symbol_(";") 771 return self.ast.MarkLigPosStatement(ligatures, marks, location=location) 772 773 def parse_position_mark_(self, enumerated, vertical): 774 location = self.cur_token_location_ 775 self.expect_keyword_("mark") 776 if enumerated: 777 raise FeatureLibError( 778 '"enumerate" is not allowed with ' 779 "mark-to-mark attachment positioning", 780 location, 781 ) 782 baseMarks = self.parse_glyphclass_(accept_glyphname=True) 783 marks = self.parse_anchor_marks_() 784 self.expect_symbol_(";") 785 return self.ast.MarkMarkPosStatement(baseMarks, marks, location=location) 786 787 def parse_script_(self): 788 assert self.is_cur_keyword_("script") 789 location, script = self.cur_token_location_, self.expect_script_tag_() 790 self.expect_symbol_(";") 791 return self.ast.ScriptStatement(script, location=location) 792 793 def parse_substitute_(self): 794 assert self.cur_token_ in {"substitute", "sub", "reversesub", "rsub"} 795 location = self.cur_token_location_ 796 reverse = self.cur_token_ in {"reversesub", "rsub"} 797 ( 798 old_prefix, 799 old, 800 lookups, 801 values, 802 old_suffix, 803 hasMarks, 804 ) = self.parse_glyph_pattern_(vertical=False) 805 if any(values): 806 raise FeatureLibError( 807 "Substitution statements cannot contain values", location 808 ) 809 new = [] 810 if self.next_token_ == "by": 811 keyword = self.expect_keyword_("by") 812 while self.next_token_ != ";": 813 gc = self.parse_glyphclass_(accept_glyphname=True, accept_null=True) 814 new.append(gc) 815 elif self.next_token_ == "from": 816 keyword = self.expect_keyword_("from") 817 new = [self.parse_glyphclass_(accept_glyphname=False)] 818 else: 819 keyword = None 820 self.expect_symbol_(";") 821 if len(new) == 0 and not any(lookups): 822 raise FeatureLibError( 823 'Expected "by", "from" or explicit lookup references', 824 self.cur_token_location_, 825 ) 826 827 # GSUB lookup type 3: Alternate substitution. 828 # Format: "substitute a from [a.1 a.2 a.3];" 829 if keyword == "from": 830 if reverse: 831 raise FeatureLibError( 832 'Reverse chaining substitutions do not support "from"', location 833 ) 834 if len(old) != 1 or len(old[0].glyphSet()) != 1: 835 raise FeatureLibError('Expected a single glyph before "from"', location) 836 if len(new) != 1: 837 raise FeatureLibError( 838 'Expected a single glyphclass after "from"', location 839 ) 840 return self.ast.AlternateSubstStatement( 841 old_prefix, old[0], old_suffix, new[0], location=location 842 ) 843 844 num_lookups = len([l for l in lookups if l is not None]) 845 846 is_deletion = False 847 if len(new) == 1 and len(new[0].glyphSet()) == 0: 848 new = [] # Deletion 849 is_deletion = True 850 851 # GSUB lookup type 1: Single substitution. 852 # Format A: "substitute a by a.sc;" 853 # Format B: "substitute [one.fitted one.oldstyle] by one;" 854 # Format C: "substitute [a-d] by [A.sc-D.sc];" 855 if not reverse and len(old) == 1 and len(new) == 1 and num_lookups == 0: 856 glyphs = list(old[0].glyphSet()) 857 replacements = list(new[0].glyphSet()) 858 if len(replacements) == 1: 859 replacements = replacements * len(glyphs) 860 if len(glyphs) != len(replacements): 861 raise FeatureLibError( 862 'Expected a glyph class with %d elements after "by", ' 863 "but found a glyph class with %d elements" 864 % (len(glyphs), len(replacements)), 865 location, 866 ) 867 return self.ast.SingleSubstStatement( 868 old, new, old_prefix, old_suffix, forceChain=hasMarks, location=location 869 ) 870 871 # GSUB lookup type 2: Multiple substitution. 872 # Format: "substitute f_f_i by f f i;" 873 if ( 874 not reverse 875 and len(old) == 1 876 and len(old[0].glyphSet()) == 1 877 and ( 878 (len(new) > 1 and max([len(n.glyphSet()) for n in new]) == 1) 879 or len(new) == 0 880 ) 881 and num_lookups == 0 882 ): 883 return self.ast.MultipleSubstStatement( 884 old_prefix, 885 tuple(old[0].glyphSet())[0], 886 old_suffix, 887 tuple([list(n.glyphSet())[0] for n in new]), 888 forceChain=hasMarks, 889 location=location, 890 ) 891 892 # GSUB lookup type 4: Ligature substitution. 893 # Format: "substitute f f i by f_f_i;" 894 if ( 895 not reverse 896 and len(old) > 1 897 and len(new) == 1 898 and len(new[0].glyphSet()) == 1 899 and num_lookups == 0 900 ): 901 return self.ast.LigatureSubstStatement( 902 old_prefix, 903 old, 904 old_suffix, 905 list(new[0].glyphSet())[0], 906 forceChain=hasMarks, 907 location=location, 908 ) 909 910 # GSUB lookup type 8: Reverse chaining substitution. 911 if reverse: 912 if len(old) != 1: 913 raise FeatureLibError( 914 "In reverse chaining single substitutions, " 915 "only a single glyph or glyph class can be replaced", 916 location, 917 ) 918 if len(new) != 1: 919 raise FeatureLibError( 920 "In reverse chaining single substitutions, " 921 'the replacement (after "by") must be a single glyph ' 922 "or glyph class", 923 location, 924 ) 925 if num_lookups != 0: 926 raise FeatureLibError( 927 "Reverse chaining substitutions cannot call named lookups", location 928 ) 929 glyphs = sorted(list(old[0].glyphSet())) 930 replacements = sorted(list(new[0].glyphSet())) 931 if len(replacements) == 1: 932 replacements = replacements * len(glyphs) 933 if len(glyphs) != len(replacements): 934 raise FeatureLibError( 935 'Expected a glyph class with %d elements after "by", ' 936 "but found a glyph class with %d elements" 937 % (len(glyphs), len(replacements)), 938 location, 939 ) 940 return self.ast.ReverseChainSingleSubstStatement( 941 old_prefix, old_suffix, old, new, location=location 942 ) 943 944 if len(old) > 1 and len(new) > 1: 945 raise FeatureLibError( 946 "Direct substitution of multiple glyphs by multiple glyphs " 947 "is not supported", 948 location, 949 ) 950 951 # If there are remaining glyphs to parse, this is an invalid GSUB statement 952 if len(new) != 0 or is_deletion: 953 raise FeatureLibError("Invalid substitution statement", location) 954 955 # GSUB lookup type 6: Chaining contextual substitution. 956 rule = self.ast.ChainContextSubstStatement( 957 old_prefix, old, old_suffix, lookups, location=location 958 ) 959 return rule 960 961 def parse_subtable_(self): 962 assert self.is_cur_keyword_("subtable") 963 location = self.cur_token_location_ 964 self.expect_symbol_(";") 965 return self.ast.SubtableStatement(location=location) 966 967 def parse_size_parameters_(self): 968 # Parses a ``parameters`` statement used in ``size`` features. See 969 # `section 8.b <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#8.b>`_. 970 assert self.is_cur_keyword_("parameters") 971 location = self.cur_token_location_ 972 DesignSize = self.expect_decipoint_() 973 SubfamilyID = self.expect_number_() 974 RangeStart = 0. 975 RangeEnd = 0. 976 if self.next_token_type_ in (Lexer.NUMBER, Lexer.FLOAT) or SubfamilyID != 0: 977 RangeStart = self.expect_decipoint_() 978 RangeEnd = self.expect_decipoint_() 979 980 self.expect_symbol_(";") 981 return self.ast.SizeParameters( 982 DesignSize, SubfamilyID, RangeStart, RangeEnd, location=location 983 ) 984 985 def parse_size_menuname_(self): 986 assert self.is_cur_keyword_("sizemenuname") 987 location = self.cur_token_location_ 988 platformID, platEncID, langID, string = self.parse_name_() 989 return self.ast.FeatureNameStatement( 990 "size", platformID, platEncID, langID, string, location=location 991 ) 992 993 def parse_table_(self): 994 assert self.is_cur_keyword_("table") 995 location, name = self.cur_token_location_, self.expect_tag_() 996 table = self.ast.TableBlock(name, location=location) 997 self.expect_symbol_("{") 998 handler = { 999 "GDEF": self.parse_table_GDEF_, 1000 "head": self.parse_table_head_, 1001 "hhea": self.parse_table_hhea_, 1002 "vhea": self.parse_table_vhea_, 1003 "name": self.parse_table_name_, 1004 "BASE": self.parse_table_BASE_, 1005 "OS/2": self.parse_table_OS_2_, 1006 "STAT": self.parse_table_STAT_, 1007 }.get(name) 1008 if handler: 1009 handler(table) 1010 else: 1011 raise FeatureLibError( 1012 '"table %s" is not supported' % name.strip(), location 1013 ) 1014 self.expect_symbol_("}") 1015 end_tag = self.expect_tag_() 1016 if end_tag != name: 1017 raise FeatureLibError( 1018 'Expected "%s"' % name.strip(), self.cur_token_location_ 1019 ) 1020 self.expect_symbol_(";") 1021 return table 1022 1023 def parse_table_GDEF_(self, table): 1024 statements = table.statements 1025 while self.next_token_ != "}" or self.cur_comments_: 1026 self.advance_lexer_(comments=True) 1027 if self.cur_token_type_ is Lexer.COMMENT: 1028 statements.append( 1029 self.ast.Comment(self.cur_token_, location=self.cur_token_location_) 1030 ) 1031 elif self.is_cur_keyword_("Attach"): 1032 statements.append(self.parse_attach_()) 1033 elif self.is_cur_keyword_("GlyphClassDef"): 1034 statements.append(self.parse_GlyphClassDef_()) 1035 elif self.is_cur_keyword_("LigatureCaretByIndex"): 1036 statements.append(self.parse_ligatureCaretByIndex_()) 1037 elif self.is_cur_keyword_("LigatureCaretByPos"): 1038 statements.append(self.parse_ligatureCaretByPos_()) 1039 elif self.cur_token_ == ";": 1040 continue 1041 else: 1042 raise FeatureLibError( 1043 "Expected Attach, LigatureCaretByIndex, " "or LigatureCaretByPos", 1044 self.cur_token_location_, 1045 ) 1046 1047 def parse_table_head_(self, table): 1048 statements = table.statements 1049 while self.next_token_ != "}" or self.cur_comments_: 1050 self.advance_lexer_(comments=True) 1051 if self.cur_token_type_ is Lexer.COMMENT: 1052 statements.append( 1053 self.ast.Comment(self.cur_token_, location=self.cur_token_location_) 1054 ) 1055 elif self.is_cur_keyword_("FontRevision"): 1056 statements.append(self.parse_FontRevision_()) 1057 elif self.cur_token_ == ";": 1058 continue 1059 else: 1060 raise FeatureLibError("Expected FontRevision", self.cur_token_location_) 1061 1062 def parse_table_hhea_(self, table): 1063 statements = table.statements 1064 fields = ("CaretOffset", "Ascender", "Descender", "LineGap") 1065 while self.next_token_ != "}" or self.cur_comments_: 1066 self.advance_lexer_(comments=True) 1067 if self.cur_token_type_ is Lexer.COMMENT: 1068 statements.append( 1069 self.ast.Comment(self.cur_token_, location=self.cur_token_location_) 1070 ) 1071 elif self.cur_token_type_ is Lexer.NAME and self.cur_token_ in fields: 1072 key = self.cur_token_.lower() 1073 value = self.expect_number_() 1074 statements.append( 1075 self.ast.HheaField(key, value, location=self.cur_token_location_) 1076 ) 1077 if self.next_token_ != ";": 1078 raise FeatureLibError( 1079 "Incomplete statement", self.next_token_location_ 1080 ) 1081 elif self.cur_token_ == ";": 1082 continue 1083 else: 1084 raise FeatureLibError( 1085 "Expected CaretOffset, Ascender, " "Descender or LineGap", 1086 self.cur_token_location_, 1087 ) 1088 1089 def parse_table_vhea_(self, table): 1090 statements = table.statements 1091 fields = ("VertTypoAscender", "VertTypoDescender", "VertTypoLineGap") 1092 while self.next_token_ != "}" or self.cur_comments_: 1093 self.advance_lexer_(comments=True) 1094 if self.cur_token_type_ is Lexer.COMMENT: 1095 statements.append( 1096 self.ast.Comment(self.cur_token_, location=self.cur_token_location_) 1097 ) 1098 elif self.cur_token_type_ is Lexer.NAME and self.cur_token_ in fields: 1099 key = self.cur_token_.lower() 1100 value = self.expect_number_() 1101 statements.append( 1102 self.ast.VheaField(key, value, location=self.cur_token_location_) 1103 ) 1104 if self.next_token_ != ";": 1105 raise FeatureLibError( 1106 "Incomplete statement", self.next_token_location_ 1107 ) 1108 elif self.cur_token_ == ";": 1109 continue 1110 else: 1111 raise FeatureLibError( 1112 "Expected VertTypoAscender, " 1113 "VertTypoDescender or VertTypoLineGap", 1114 self.cur_token_location_, 1115 ) 1116 1117 def parse_table_name_(self, table): 1118 statements = table.statements 1119 while self.next_token_ != "}" or self.cur_comments_: 1120 self.advance_lexer_(comments=True) 1121 if self.cur_token_type_ is Lexer.COMMENT: 1122 statements.append( 1123 self.ast.Comment(self.cur_token_, location=self.cur_token_location_) 1124 ) 1125 elif self.is_cur_keyword_("nameid"): 1126 statement = self.parse_nameid_() 1127 if statement: 1128 statements.append(statement) 1129 elif self.cur_token_ == ";": 1130 continue 1131 else: 1132 raise FeatureLibError("Expected nameid", self.cur_token_location_) 1133 1134 def parse_name_(self): 1135 """Parses a name record. See `section 9.e <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.e>`_.""" 1136 platEncID = None 1137 langID = None 1138 if self.next_token_type_ in Lexer.NUMBERS: 1139 platformID = self.expect_any_number_() 1140 location = self.cur_token_location_ 1141 if platformID not in (1, 3): 1142 raise FeatureLibError("Expected platform id 1 or 3", location) 1143 if self.next_token_type_ in Lexer.NUMBERS: 1144 platEncID = self.expect_any_number_() 1145 langID = self.expect_any_number_() 1146 else: 1147 platformID = 3 1148 location = self.cur_token_location_ 1149 1150 if platformID == 1: # Macintosh 1151 platEncID = platEncID or 0 # Roman 1152 langID = langID or 0 # English 1153 else: # 3, Windows 1154 platEncID = platEncID or 1 # Unicode 1155 langID = langID or 0x0409 # English 1156 1157 string = self.expect_string_() 1158 self.expect_symbol_(";") 1159 1160 encoding = getEncoding(platformID, platEncID, langID) 1161 if encoding is None: 1162 raise FeatureLibError("Unsupported encoding", location) 1163 unescaped = self.unescape_string_(string, encoding) 1164 return platformID, platEncID, langID, unescaped 1165 1166 def parse_stat_name_(self): 1167 platEncID = None 1168 langID = None 1169 if self.next_token_type_ in Lexer.NUMBERS: 1170 platformID = self.expect_any_number_() 1171 location = self.cur_token_location_ 1172 if platformID not in (1, 3): 1173 raise FeatureLibError("Expected platform id 1 or 3", location) 1174 if self.next_token_type_ in Lexer.NUMBERS: 1175 platEncID = self.expect_any_number_() 1176 langID = self.expect_any_number_() 1177 else: 1178 platformID = 3 1179 location = self.cur_token_location_ 1180 1181 if platformID == 1: # Macintosh 1182 platEncID = platEncID or 0 # Roman 1183 langID = langID or 0 # English 1184 else: # 3, Windows 1185 platEncID = platEncID or 1 # Unicode 1186 langID = langID or 0x0409 # English 1187 1188 string = self.expect_string_() 1189 encoding = getEncoding(platformID, platEncID, langID) 1190 if encoding is None: 1191 raise FeatureLibError("Unsupported encoding", location) 1192 unescaped = self.unescape_string_(string, encoding) 1193 return platformID, platEncID, langID, unescaped 1194 1195 def parse_nameid_(self): 1196 assert self.cur_token_ == "nameid", self.cur_token_ 1197 location, nameID = self.cur_token_location_, self.expect_any_number_() 1198 if nameID > 32767: 1199 raise FeatureLibError( 1200 "Name id value cannot be greater than 32767", self.cur_token_location_ 1201 ) 1202 if 1 <= nameID <= 6: 1203 log.warning( 1204 "Name id %d cannot be set from the feature file. " 1205 "Ignoring record" % nameID 1206 ) 1207 self.parse_name_() # skip to the next record 1208 return None 1209 1210 platformID, platEncID, langID, string = self.parse_name_() 1211 return self.ast.NameRecord( 1212 nameID, platformID, platEncID, langID, string, location=location 1213 ) 1214 1215 def unescape_string_(self, string, encoding): 1216 if encoding == "utf_16_be": 1217 s = re.sub(r"\\[0-9a-fA-F]{4}", self.unescape_unichr_, string) 1218 else: 1219 unescape = lambda m: self.unescape_byte_(m, encoding) 1220 s = re.sub(r"\\[0-9a-fA-F]{2}", unescape, string) 1221 # We now have a Unicode string, but it might contain surrogate pairs. 1222 # We convert surrogates to actual Unicode by round-tripping through 1223 # Python's UTF-16 codec in a special mode. 1224 utf16 = tobytes(s, "utf_16_be", "surrogatepass") 1225 return tostr(utf16, "utf_16_be") 1226 1227 @staticmethod 1228 def unescape_unichr_(match): 1229 n = match.group(0)[1:] 1230 return chr(int(n, 16)) 1231 1232 @staticmethod 1233 def unescape_byte_(match, encoding): 1234 n = match.group(0)[1:] 1235 return bytechr(int(n, 16)).decode(encoding) 1236 1237 def parse_table_BASE_(self, table): 1238 statements = table.statements 1239 while self.next_token_ != "}" or self.cur_comments_: 1240 self.advance_lexer_(comments=True) 1241 if self.cur_token_type_ is Lexer.COMMENT: 1242 statements.append( 1243 self.ast.Comment(self.cur_token_, location=self.cur_token_location_) 1244 ) 1245 elif self.is_cur_keyword_("HorizAxis.BaseTagList"): 1246 horiz_bases = self.parse_base_tag_list_() 1247 elif self.is_cur_keyword_("HorizAxis.BaseScriptList"): 1248 horiz_scripts = self.parse_base_script_list_(len(horiz_bases)) 1249 statements.append( 1250 self.ast.BaseAxis( 1251 horiz_bases, 1252 horiz_scripts, 1253 False, 1254 location=self.cur_token_location_, 1255 ) 1256 ) 1257 elif self.is_cur_keyword_("VertAxis.BaseTagList"): 1258 vert_bases = self.parse_base_tag_list_() 1259 elif self.is_cur_keyword_("VertAxis.BaseScriptList"): 1260 vert_scripts = self.parse_base_script_list_(len(vert_bases)) 1261 statements.append( 1262 self.ast.BaseAxis( 1263 vert_bases, 1264 vert_scripts, 1265 True, 1266 location=self.cur_token_location_, 1267 ) 1268 ) 1269 elif self.cur_token_ == ";": 1270 continue 1271 1272 def parse_table_OS_2_(self, table): 1273 statements = table.statements 1274 numbers = ( 1275 "FSType", 1276 "TypoAscender", 1277 "TypoDescender", 1278 "TypoLineGap", 1279 "winAscent", 1280 "winDescent", 1281 "XHeight", 1282 "CapHeight", 1283 "WeightClass", 1284 "WidthClass", 1285 "LowerOpSize", 1286 "UpperOpSize", 1287 ) 1288 ranges = ("UnicodeRange", "CodePageRange") 1289 while self.next_token_ != "}" or self.cur_comments_: 1290 self.advance_lexer_(comments=True) 1291 if self.cur_token_type_ is Lexer.COMMENT: 1292 statements.append( 1293 self.ast.Comment(self.cur_token_, location=self.cur_token_location_) 1294 ) 1295 elif self.cur_token_type_ is Lexer.NAME: 1296 key = self.cur_token_.lower() 1297 value = None 1298 if self.cur_token_ in numbers: 1299 value = self.expect_number_() 1300 elif self.is_cur_keyword_("Panose"): 1301 value = [] 1302 for i in range(10): 1303 value.append(self.expect_number_()) 1304 elif self.cur_token_ in ranges: 1305 value = [] 1306 while self.next_token_ != ";": 1307 value.append(self.expect_number_()) 1308 elif self.is_cur_keyword_("Vendor"): 1309 value = self.expect_string_() 1310 statements.append( 1311 self.ast.OS2Field(key, value, location=self.cur_token_location_) 1312 ) 1313 elif self.cur_token_ == ";": 1314 continue 1315 1316 def parse_STAT_ElidedFallbackName(self): 1317 assert self.is_cur_keyword_("ElidedFallbackName") 1318 self.expect_symbol_("{") 1319 names = [] 1320 while self.next_token_ != "}" or self.cur_comments_: 1321 self.advance_lexer_() 1322 if self.is_cur_keyword_("name"): 1323 platformID, platEncID, langID, string = self.parse_stat_name_() 1324 nameRecord = self.ast.STATNameStatement( 1325 "stat", 1326 platformID, 1327 platEncID, 1328 langID, 1329 string, 1330 location=self.cur_token_location_, 1331 ) 1332 names.append(nameRecord) 1333 else: 1334 if self.cur_token_ != ";": 1335 raise FeatureLibError( 1336 f"Unexpected token {self.cur_token_} " f"in ElidedFallbackName", 1337 self.cur_token_location_, 1338 ) 1339 self.expect_symbol_("}") 1340 if not names: 1341 raise FeatureLibError('Expected "name"', self.cur_token_location_) 1342 return names 1343 1344 def parse_STAT_design_axis(self): 1345 assert self.is_cur_keyword_("DesignAxis") 1346 names = [] 1347 axisTag = self.expect_tag_() 1348 if ( 1349 axisTag not in ("ital", "opsz", "slnt", "wdth", "wght") 1350 and not axisTag.isupper() 1351 ): 1352 log.warning(f"Unregistered axis tag {axisTag} should be uppercase.") 1353 axisOrder = self.expect_number_() 1354 self.expect_symbol_("{") 1355 while self.next_token_ != "}" or self.cur_comments_: 1356 self.advance_lexer_() 1357 if self.cur_token_type_ is Lexer.COMMENT: 1358 continue 1359 elif self.is_cur_keyword_("name"): 1360 location = self.cur_token_location_ 1361 platformID, platEncID, langID, string = self.parse_stat_name_() 1362 name = self.ast.STATNameStatement( 1363 "stat", platformID, platEncID, langID, string, location=location 1364 ) 1365 names.append(name) 1366 elif self.cur_token_ == ";": 1367 continue 1368 else: 1369 raise FeatureLibError( 1370 f'Expected "name", got {self.cur_token_}', self.cur_token_location_ 1371 ) 1372 1373 self.expect_symbol_("}") 1374 return self.ast.STATDesignAxisStatement( 1375 axisTag, axisOrder, names, self.cur_token_location_ 1376 ) 1377 1378 def parse_STAT_axis_value_(self): 1379 assert self.is_cur_keyword_("AxisValue") 1380 self.expect_symbol_("{") 1381 locations = [] 1382 names = [] 1383 flags = 0 1384 while self.next_token_ != "}" or self.cur_comments_: 1385 self.advance_lexer_(comments=True) 1386 if self.cur_token_type_ is Lexer.COMMENT: 1387 continue 1388 elif self.is_cur_keyword_("name"): 1389 location = self.cur_token_location_ 1390 platformID, platEncID, langID, string = self.parse_stat_name_() 1391 name = self.ast.STATNameStatement( 1392 "stat", platformID, platEncID, langID, string, location=location 1393 ) 1394 names.append(name) 1395 elif self.is_cur_keyword_("location"): 1396 location = self.parse_STAT_location() 1397 locations.append(location) 1398 elif self.is_cur_keyword_("flag"): 1399 flags = self.expect_stat_flags() 1400 elif self.cur_token_ == ";": 1401 continue 1402 else: 1403 raise FeatureLibError( 1404 f"Unexpected token {self.cur_token_} " f"in AxisValue", 1405 self.cur_token_location_, 1406 ) 1407 self.expect_symbol_("}") 1408 if not names: 1409 raise FeatureLibError('Expected "Axis Name"', self.cur_token_location_) 1410 if not locations: 1411 raise FeatureLibError('Expected "Axis location"', self.cur_token_location_) 1412 if len(locations) > 1: 1413 for location in locations: 1414 if len(location.values) > 1: 1415 raise FeatureLibError( 1416 "Only one value is allowed in a " 1417 "Format 4 Axis Value Record, but " 1418 f"{len(location.values)} were found.", 1419 self.cur_token_location_, 1420 ) 1421 format4_tags = [] 1422 for location in locations: 1423 tag = location.tag 1424 if tag in format4_tags: 1425 raise FeatureLibError( 1426 f"Axis tag {tag} already " "defined.", self.cur_token_location_ 1427 ) 1428 format4_tags.append(tag) 1429 1430 return self.ast.STATAxisValueStatement( 1431 names, locations, flags, self.cur_token_location_ 1432 ) 1433 1434 def parse_STAT_location(self): 1435 values = [] 1436 tag = self.expect_tag_() 1437 if len(tag.strip()) != 4: 1438 raise FeatureLibError( 1439 f"Axis tag {self.cur_token_} must be 4 " "characters", 1440 self.cur_token_location_, 1441 ) 1442 1443 while self.next_token_ != ";": 1444 if self.next_token_type_ is Lexer.FLOAT: 1445 value = self.expect_float_() 1446 values.append(value) 1447 elif self.next_token_type_ is Lexer.NUMBER: 1448 value = self.expect_number_() 1449 values.append(value) 1450 else: 1451 raise FeatureLibError( 1452 f'Unexpected value "{self.next_token_}". ' 1453 "Expected integer or float.", 1454 self.next_token_location_, 1455 ) 1456 if len(values) == 3: 1457 nominal, min_val, max_val = values 1458 if nominal < min_val or nominal > max_val: 1459 raise FeatureLibError( 1460 f"Default value {nominal} is outside " 1461 f"of specified range " 1462 f"{min_val}-{max_val}.", 1463 self.next_token_location_, 1464 ) 1465 return self.ast.AxisValueLocationStatement(tag, values) 1466 1467 def parse_table_STAT_(self, table): 1468 statements = table.statements 1469 design_axes = [] 1470 while self.next_token_ != "}" or self.cur_comments_: 1471 self.advance_lexer_(comments=True) 1472 if self.cur_token_type_ is Lexer.COMMENT: 1473 statements.append( 1474 self.ast.Comment(self.cur_token_, location=self.cur_token_location_) 1475 ) 1476 elif self.cur_token_type_ is Lexer.NAME: 1477 if self.is_cur_keyword_("ElidedFallbackName"): 1478 names = self.parse_STAT_ElidedFallbackName() 1479 statements.append(self.ast.ElidedFallbackName(names)) 1480 elif self.is_cur_keyword_("ElidedFallbackNameID"): 1481 value = self.expect_number_() 1482 statements.append(self.ast.ElidedFallbackNameID(value)) 1483 self.expect_symbol_(";") 1484 elif self.is_cur_keyword_("DesignAxis"): 1485 designAxis = self.parse_STAT_design_axis() 1486 design_axes.append(designAxis.tag) 1487 statements.append(designAxis) 1488 self.expect_symbol_(";") 1489 elif self.is_cur_keyword_("AxisValue"): 1490 axisValueRecord = self.parse_STAT_axis_value_() 1491 for location in axisValueRecord.locations: 1492 if location.tag not in design_axes: 1493 # Tag must be defined in a DesignAxis before it 1494 # can be referenced 1495 raise FeatureLibError( 1496 "DesignAxis not defined for " f"{location.tag}.", 1497 self.cur_token_location_, 1498 ) 1499 statements.append(axisValueRecord) 1500 self.expect_symbol_(";") 1501 else: 1502 raise FeatureLibError( 1503 f"Unexpected token {self.cur_token_}", self.cur_token_location_ 1504 ) 1505 elif self.cur_token_ == ";": 1506 continue 1507 1508 def parse_base_tag_list_(self): 1509 # Parses BASE table entries. (See `section 9.a <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.a>`_) 1510 assert self.cur_token_ in ( 1511 "HorizAxis.BaseTagList", 1512 "VertAxis.BaseTagList", 1513 ), self.cur_token_ 1514 bases = [] 1515 while self.next_token_ != ";": 1516 bases.append(self.expect_script_tag_()) 1517 self.expect_symbol_(";") 1518 return bases 1519 1520 def parse_base_script_list_(self, count): 1521 assert self.cur_token_ in ( 1522 "HorizAxis.BaseScriptList", 1523 "VertAxis.BaseScriptList", 1524 ), self.cur_token_ 1525 scripts = [(self.parse_base_script_record_(count))] 1526 while self.next_token_ == ",": 1527 self.expect_symbol_(",") 1528 scripts.append(self.parse_base_script_record_(count)) 1529 self.expect_symbol_(";") 1530 return scripts 1531 1532 def parse_base_script_record_(self, count): 1533 script_tag = self.expect_script_tag_() 1534 base_tag = self.expect_script_tag_() 1535 coords = [self.expect_number_() for i in range(count)] 1536 return script_tag, base_tag, coords 1537 1538 def parse_device_(self): 1539 result = None 1540 self.expect_symbol_("<") 1541 self.expect_keyword_("device") 1542 if self.next_token_ == "NULL": 1543 self.expect_keyword_("NULL") 1544 else: 1545 result = [(self.expect_number_(), self.expect_number_())] 1546 while self.next_token_ == ",": 1547 self.expect_symbol_(",") 1548 result.append((self.expect_number_(), self.expect_number_())) 1549 result = tuple(result) # make it hashable 1550 self.expect_symbol_(">") 1551 return result 1552 1553 def is_next_value_(self): 1554 return self.next_token_type_ is Lexer.NUMBER or self.next_token_ == "<" 1555 1556 def parse_valuerecord_(self, vertical): 1557 if self.next_token_type_ is Lexer.NUMBER: 1558 number, location = self.expect_number_(), self.cur_token_location_ 1559 if vertical: 1560 val = self.ast.ValueRecord( 1561 yAdvance=number, vertical=vertical, location=location 1562 ) 1563 else: 1564 val = self.ast.ValueRecord( 1565 xAdvance=number, vertical=vertical, location=location 1566 ) 1567 return val 1568 self.expect_symbol_("<") 1569 location = self.cur_token_location_ 1570 if self.next_token_type_ is Lexer.NAME: 1571 name = self.expect_name_() 1572 if name == "NULL": 1573 self.expect_symbol_(">") 1574 return self.ast.ValueRecord() 1575 vrd = self.valuerecords_.resolve(name) 1576 if vrd is None: 1577 raise FeatureLibError( 1578 'Unknown valueRecordDef "%s"' % name, self.cur_token_location_ 1579 ) 1580 value = vrd.value 1581 xPlacement, yPlacement = (value.xPlacement, value.yPlacement) 1582 xAdvance, yAdvance = (value.xAdvance, value.yAdvance) 1583 else: 1584 xPlacement, yPlacement, xAdvance, yAdvance = ( 1585 self.expect_number_(), 1586 self.expect_number_(), 1587 self.expect_number_(), 1588 self.expect_number_(), 1589 ) 1590 1591 if self.next_token_ == "<": 1592 xPlaDevice, yPlaDevice, xAdvDevice, yAdvDevice = ( 1593 self.parse_device_(), 1594 self.parse_device_(), 1595 self.parse_device_(), 1596 self.parse_device_(), 1597 ) 1598 allDeltas = sorted( 1599 [ 1600 delta 1601 for size, delta in (xPlaDevice if xPlaDevice else ()) 1602 + (yPlaDevice if yPlaDevice else ()) 1603 + (xAdvDevice if xAdvDevice else ()) 1604 + (yAdvDevice if yAdvDevice else ()) 1605 ] 1606 ) 1607 if allDeltas[0] < -128 or allDeltas[-1] > 127: 1608 raise FeatureLibError( 1609 "Device value out of valid range (-128..127)", 1610 self.cur_token_location_, 1611 ) 1612 else: 1613 xPlaDevice, yPlaDevice, xAdvDevice, yAdvDevice = (None, None, None, None) 1614 1615 self.expect_symbol_(">") 1616 return self.ast.ValueRecord( 1617 xPlacement, 1618 yPlacement, 1619 xAdvance, 1620 yAdvance, 1621 xPlaDevice, 1622 yPlaDevice, 1623 xAdvDevice, 1624 yAdvDevice, 1625 vertical=vertical, 1626 location=location, 1627 ) 1628 1629 def parse_valuerecord_definition_(self, vertical): 1630 # Parses a named value record definition. (See section `2.e.v <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#2.e.v>`_) 1631 assert self.is_cur_keyword_("valueRecordDef") 1632 location = self.cur_token_location_ 1633 value = self.parse_valuerecord_(vertical) 1634 name = self.expect_name_() 1635 self.expect_symbol_(";") 1636 vrd = self.ast.ValueRecordDefinition(name, value, location=location) 1637 self.valuerecords_.define(name, vrd) 1638 return vrd 1639 1640 def parse_languagesystem_(self): 1641 assert self.cur_token_ == "languagesystem" 1642 location = self.cur_token_location_ 1643 script = self.expect_script_tag_() 1644 language = self.expect_language_tag_() 1645 self.expect_symbol_(";") 1646 return self.ast.LanguageSystemStatement(script, language, location=location) 1647 1648 def parse_feature_block_(self): 1649 assert self.cur_token_ == "feature" 1650 location = self.cur_token_location_ 1651 tag = self.expect_tag_() 1652 vertical = tag in {"vkrn", "vpal", "vhal", "valt"} 1653 1654 stylisticset = None 1655 cv_feature = None 1656 size_feature = False 1657 if tag in self.SS_FEATURE_TAGS: 1658 stylisticset = tag 1659 elif tag in self.CV_FEATURE_TAGS: 1660 cv_feature = tag 1661 elif tag == "size": 1662 size_feature = True 1663 1664 use_extension = False 1665 if self.next_token_ == "useExtension": 1666 self.expect_keyword_("useExtension") 1667 use_extension = True 1668 1669 block = self.ast.FeatureBlock( 1670 tag, use_extension=use_extension, location=location 1671 ) 1672 self.parse_block_(block, vertical, stylisticset, size_feature, cv_feature) 1673 return block 1674 1675 def parse_feature_reference_(self): 1676 assert self.cur_token_ == "feature", self.cur_token_ 1677 location = self.cur_token_location_ 1678 featureName = self.expect_tag_() 1679 self.expect_symbol_(";") 1680 return self.ast.FeatureReferenceStatement(featureName, location=location) 1681 1682 def parse_featureNames_(self, tag): 1683 """Parses a ``featureNames`` statement found in stylistic set features. 1684 See section `8.c <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#8.c>`_.""" 1685 assert self.cur_token_ == "featureNames", self.cur_token_ 1686 block = self.ast.NestedBlock( 1687 tag, self.cur_token_, location=self.cur_token_location_ 1688 ) 1689 self.expect_symbol_("{") 1690 for symtab in self.symbol_tables_: 1691 symtab.enter_scope() 1692 while self.next_token_ != "}" or self.cur_comments_: 1693 self.advance_lexer_(comments=True) 1694 if self.cur_token_type_ is Lexer.COMMENT: 1695 block.statements.append( 1696 self.ast.Comment(self.cur_token_, location=self.cur_token_location_) 1697 ) 1698 elif self.is_cur_keyword_("name"): 1699 location = self.cur_token_location_ 1700 platformID, platEncID, langID, string = self.parse_name_() 1701 block.statements.append( 1702 self.ast.FeatureNameStatement( 1703 tag, platformID, platEncID, langID, string, location=location 1704 ) 1705 ) 1706 elif self.cur_token_ == ";": 1707 continue 1708 else: 1709 raise FeatureLibError('Expected "name"', self.cur_token_location_) 1710 self.expect_symbol_("}") 1711 for symtab in self.symbol_tables_: 1712 symtab.exit_scope() 1713 self.expect_symbol_(";") 1714 return block 1715 1716 def parse_cvParameters_(self, tag): 1717 # Parses a ``cvParameters`` block found in Character Variant features. 1718 # See section `8.d <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#8.d>`_. 1719 assert self.cur_token_ == "cvParameters", self.cur_token_ 1720 block = self.ast.NestedBlock( 1721 tag, self.cur_token_, location=self.cur_token_location_ 1722 ) 1723 self.expect_symbol_("{") 1724 for symtab in self.symbol_tables_: 1725 symtab.enter_scope() 1726 1727 statements = block.statements 1728 while self.next_token_ != "}" or self.cur_comments_: 1729 self.advance_lexer_(comments=True) 1730 if self.cur_token_type_ is Lexer.COMMENT: 1731 statements.append( 1732 self.ast.Comment(self.cur_token_, location=self.cur_token_location_) 1733 ) 1734 elif self.is_cur_keyword_( 1735 { 1736 "FeatUILabelNameID", 1737 "FeatUITooltipTextNameID", 1738 "SampleTextNameID", 1739 "ParamUILabelNameID", 1740 } 1741 ): 1742 statements.append(self.parse_cvNameIDs_(tag, self.cur_token_)) 1743 elif self.is_cur_keyword_("Character"): 1744 statements.append(self.parse_cvCharacter_(tag)) 1745 elif self.cur_token_ == ";": 1746 continue 1747 else: 1748 raise FeatureLibError( 1749 "Expected statement: got {} {}".format( 1750 self.cur_token_type_, self.cur_token_ 1751 ), 1752 self.cur_token_location_, 1753 ) 1754 1755 self.expect_symbol_("}") 1756 for symtab in self.symbol_tables_: 1757 symtab.exit_scope() 1758 self.expect_symbol_(";") 1759 return block 1760 1761 def parse_cvNameIDs_(self, tag, block_name): 1762 assert self.cur_token_ == block_name, self.cur_token_ 1763 block = self.ast.NestedBlock(tag, block_name, location=self.cur_token_location_) 1764 self.expect_symbol_("{") 1765 for symtab in self.symbol_tables_: 1766 symtab.enter_scope() 1767 while self.next_token_ != "}" or self.cur_comments_: 1768 self.advance_lexer_(comments=True) 1769 if self.cur_token_type_ is Lexer.COMMENT: 1770 block.statements.append( 1771 self.ast.Comment(self.cur_token_, location=self.cur_token_location_) 1772 ) 1773 elif self.is_cur_keyword_("name"): 1774 location = self.cur_token_location_ 1775 platformID, platEncID, langID, string = self.parse_name_() 1776 block.statements.append( 1777 self.ast.CVParametersNameStatement( 1778 tag, 1779 platformID, 1780 platEncID, 1781 langID, 1782 string, 1783 block_name, 1784 location=location, 1785 ) 1786 ) 1787 elif self.cur_token_ == ";": 1788 continue 1789 else: 1790 raise FeatureLibError('Expected "name"', self.cur_token_location_) 1791 self.expect_symbol_("}") 1792 for symtab in self.symbol_tables_: 1793 symtab.exit_scope() 1794 self.expect_symbol_(";") 1795 return block 1796 1797 def parse_cvCharacter_(self, tag): 1798 assert self.cur_token_ == "Character", self.cur_token_ 1799 location, character = self.cur_token_location_, self.expect_any_number_() 1800 self.expect_symbol_(";") 1801 if not (0xFFFFFF >= character >= 0): 1802 raise FeatureLibError( 1803 "Character value must be between " 1804 "{:#x} and {:#x}".format(0, 0xFFFFFF), 1805 location, 1806 ) 1807 return self.ast.CharacterStatement(character, tag, location=location) 1808 1809 def parse_FontRevision_(self): 1810 # Parses a ``FontRevision`` statement found in the head table. See 1811 # `section 9.c <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.c>`_. 1812 assert self.cur_token_ == "FontRevision", self.cur_token_ 1813 location, version = self.cur_token_location_, self.expect_float_() 1814 self.expect_symbol_(";") 1815 if version <= 0: 1816 raise FeatureLibError("Font revision numbers must be positive", location) 1817 return self.ast.FontRevisionStatement(version, location=location) 1818 1819 def parse_block_( 1820 self, block, vertical, stylisticset=None, size_feature=False, cv_feature=None 1821 ): 1822 self.expect_symbol_("{") 1823 for symtab in self.symbol_tables_: 1824 symtab.enter_scope() 1825 1826 statements = block.statements 1827 while self.next_token_ != "}" or self.cur_comments_: 1828 self.advance_lexer_(comments=True) 1829 if self.cur_token_type_ is Lexer.COMMENT: 1830 statements.append( 1831 self.ast.Comment(self.cur_token_, location=self.cur_token_location_) 1832 ) 1833 elif self.cur_token_type_ is Lexer.GLYPHCLASS: 1834 statements.append(self.parse_glyphclass_definition_()) 1835 elif self.is_cur_keyword_("anchorDef"): 1836 statements.append(self.parse_anchordef_()) 1837 elif self.is_cur_keyword_({"enum", "enumerate"}): 1838 statements.append(self.parse_enumerate_(vertical=vertical)) 1839 elif self.is_cur_keyword_("feature"): 1840 statements.append(self.parse_feature_reference_()) 1841 elif self.is_cur_keyword_("ignore"): 1842 statements.append(self.parse_ignore_()) 1843 elif self.is_cur_keyword_("language"): 1844 statements.append(self.parse_language_()) 1845 elif self.is_cur_keyword_("lookup"): 1846 statements.append(self.parse_lookup_(vertical)) 1847 elif self.is_cur_keyword_("lookupflag"): 1848 statements.append(self.parse_lookupflag_()) 1849 elif self.is_cur_keyword_("markClass"): 1850 statements.append(self.parse_markClass_()) 1851 elif self.is_cur_keyword_({"pos", "position"}): 1852 statements.append( 1853 self.parse_position_(enumerated=False, vertical=vertical) 1854 ) 1855 elif self.is_cur_keyword_("script"): 1856 statements.append(self.parse_script_()) 1857 elif self.is_cur_keyword_({"sub", "substitute", "rsub", "reversesub"}): 1858 statements.append(self.parse_substitute_()) 1859 elif self.is_cur_keyword_("subtable"): 1860 statements.append(self.parse_subtable_()) 1861 elif self.is_cur_keyword_("valueRecordDef"): 1862 statements.append(self.parse_valuerecord_definition_(vertical)) 1863 elif stylisticset and self.is_cur_keyword_("featureNames"): 1864 statements.append(self.parse_featureNames_(stylisticset)) 1865 elif cv_feature and self.is_cur_keyword_("cvParameters"): 1866 statements.append(self.parse_cvParameters_(cv_feature)) 1867 elif size_feature and self.is_cur_keyword_("parameters"): 1868 statements.append(self.parse_size_parameters_()) 1869 elif size_feature and self.is_cur_keyword_("sizemenuname"): 1870 statements.append(self.parse_size_menuname_()) 1871 elif ( 1872 self.cur_token_type_ is Lexer.NAME 1873 and self.cur_token_ in self.extensions 1874 ): 1875 statements.append(self.extensions[self.cur_token_](self)) 1876 elif self.cur_token_ == ";": 1877 continue 1878 else: 1879 raise FeatureLibError( 1880 "Expected glyph class definition or statement: got {} {}".format( 1881 self.cur_token_type_, self.cur_token_ 1882 ), 1883 self.cur_token_location_, 1884 ) 1885 1886 self.expect_symbol_("}") 1887 for symtab in self.symbol_tables_: 1888 symtab.exit_scope() 1889 1890 name = self.expect_name_() 1891 if name != block.name.strip(): 1892 raise FeatureLibError( 1893 'Expected "%s"' % block.name.strip(), self.cur_token_location_ 1894 ) 1895 self.expect_symbol_(";") 1896 1897 # A multiple substitution may have a single destination, in which case 1898 # it will look just like a single substitution. So if there are both 1899 # multiple and single substitutions, upgrade all the single ones to 1900 # multiple substitutions. 1901 1902 # Check if we have a mix of non-contextual singles and multiples. 1903 has_single = False 1904 has_multiple = False 1905 for s in statements: 1906 if isinstance(s, self.ast.SingleSubstStatement): 1907 has_single = not any([s.prefix, s.suffix, s.forceChain]) 1908 elif isinstance(s, self.ast.MultipleSubstStatement): 1909 has_multiple = not any([s.prefix, s.suffix, s.forceChain]) 1910 1911 # Upgrade all single substitutions to multiple substitutions. 1912 if has_single and has_multiple: 1913 statements = [] 1914 for s in block.statements: 1915 if isinstance(s, self.ast.SingleSubstStatement): 1916 glyphs = s.glyphs[0].glyphSet() 1917 replacements = s.replacements[0].glyphSet() 1918 if len(replacements) == 1: 1919 replacements *= len(glyphs) 1920 for i, glyph in enumerate(glyphs): 1921 statements.append( 1922 self.ast.MultipleSubstStatement( 1923 s.prefix, 1924 glyph, 1925 s.suffix, 1926 [replacements[i]], 1927 s.forceChain, 1928 location=s.location, 1929 ) 1930 ) 1931 else: 1932 statements.append(s) 1933 block.statements = statements 1934 1935 def is_cur_keyword_(self, k): 1936 if self.cur_token_type_ is Lexer.NAME: 1937 if isinstance(k, type("")): # basestring is gone in Python3 1938 return self.cur_token_ == k 1939 else: 1940 return self.cur_token_ in k 1941 return False 1942 1943 def expect_class_name_(self): 1944 self.advance_lexer_() 1945 if self.cur_token_type_ is not Lexer.GLYPHCLASS: 1946 raise FeatureLibError("Expected @NAME", self.cur_token_location_) 1947 return self.cur_token_ 1948 1949 def expect_cid_(self): 1950 self.advance_lexer_() 1951 if self.cur_token_type_ is Lexer.CID: 1952 return self.cur_token_ 1953 raise FeatureLibError("Expected a CID", self.cur_token_location_) 1954 1955 def expect_filename_(self): 1956 self.advance_lexer_() 1957 if self.cur_token_type_ is not Lexer.FILENAME: 1958 raise FeatureLibError("Expected file name", self.cur_token_location_) 1959 return self.cur_token_ 1960 1961 def expect_glyph_(self): 1962 self.advance_lexer_() 1963 if self.cur_token_type_ is Lexer.NAME: 1964 self.cur_token_ = self.cur_token_.lstrip("\\") 1965 if len(self.cur_token_) > 63: 1966 raise FeatureLibError( 1967 "Glyph names must not be longer than 63 characters", 1968 self.cur_token_location_, 1969 ) 1970 return self.cur_token_ 1971 elif self.cur_token_type_ is Lexer.CID: 1972 return "cid%05d" % self.cur_token_ 1973 raise FeatureLibError("Expected a glyph name or CID", self.cur_token_location_) 1974 1975 def check_glyph_name_in_glyph_set(self, *names): 1976 """Raises if glyph name (just `start`) or glyph names of a 1977 range (`start` and `end`) are not in the glyph set. 1978 1979 If no glyph set is present, does nothing. 1980 """ 1981 if self.glyphNames_: 1982 missing = [name for name in names if name not in self.glyphNames_] 1983 if missing: 1984 raise FeatureLibError( 1985 "The following glyph names are referenced but are missing from the " 1986 f"glyph set: {', '.join(missing)}", 1987 self.cur_token_location_, 1988 ) 1989 1990 def expect_markClass_reference_(self): 1991 name = self.expect_class_name_() 1992 mc = self.glyphclasses_.resolve(name) 1993 if mc is None: 1994 raise FeatureLibError( 1995 "Unknown markClass @%s" % name, self.cur_token_location_ 1996 ) 1997 if not isinstance(mc, self.ast.MarkClass): 1998 raise FeatureLibError( 1999 "@%s is not a markClass" % name, self.cur_token_location_ 2000 ) 2001 return mc 2002 2003 def expect_tag_(self): 2004 self.advance_lexer_() 2005 if self.cur_token_type_ is not Lexer.NAME: 2006 raise FeatureLibError("Expected a tag", self.cur_token_location_) 2007 if len(self.cur_token_) > 4: 2008 raise FeatureLibError( 2009 "Tags cannot be longer than 4 characters", self.cur_token_location_ 2010 ) 2011 return (self.cur_token_ + " ")[:4] 2012 2013 def expect_script_tag_(self): 2014 tag = self.expect_tag_() 2015 if tag == "dflt": 2016 raise FeatureLibError( 2017 '"dflt" is not a valid script tag; use "DFLT" instead', 2018 self.cur_token_location_, 2019 ) 2020 return tag 2021 2022 def expect_language_tag_(self): 2023 tag = self.expect_tag_() 2024 if tag == "DFLT": 2025 raise FeatureLibError( 2026 '"DFLT" is not a valid language tag; use "dflt" instead', 2027 self.cur_token_location_, 2028 ) 2029 return tag 2030 2031 def expect_symbol_(self, symbol): 2032 self.advance_lexer_() 2033 if self.cur_token_type_ is Lexer.SYMBOL and self.cur_token_ == symbol: 2034 return symbol 2035 raise FeatureLibError("Expected '%s'" % symbol, self.cur_token_location_) 2036 2037 def expect_keyword_(self, keyword): 2038 self.advance_lexer_() 2039 if self.cur_token_type_ is Lexer.NAME and self.cur_token_ == keyword: 2040 return self.cur_token_ 2041 raise FeatureLibError('Expected "%s"' % keyword, self.cur_token_location_) 2042 2043 def expect_name_(self): 2044 self.advance_lexer_() 2045 if self.cur_token_type_ is Lexer.NAME: 2046 return self.cur_token_ 2047 raise FeatureLibError("Expected a name", self.cur_token_location_) 2048 2049 def expect_number_(self): 2050 self.advance_lexer_() 2051 if self.cur_token_type_ is Lexer.NUMBER: 2052 return self.cur_token_ 2053 raise FeatureLibError("Expected a number", self.cur_token_location_) 2054 2055 def expect_any_number_(self): 2056 self.advance_lexer_() 2057 if self.cur_token_type_ in Lexer.NUMBERS: 2058 return self.cur_token_ 2059 raise FeatureLibError( 2060 "Expected a decimal, hexadecimal or octal number", self.cur_token_location_ 2061 ) 2062 2063 def expect_float_(self): 2064 self.advance_lexer_() 2065 if self.cur_token_type_ is Lexer.FLOAT: 2066 return self.cur_token_ 2067 raise FeatureLibError( 2068 "Expected a floating-point number", self.cur_token_location_ 2069 ) 2070 2071 def expect_decipoint_(self): 2072 if self.next_token_type_ == Lexer.FLOAT: 2073 return self.expect_float_() 2074 elif self.next_token_type_ is Lexer.NUMBER: 2075 return self.expect_number_() / 10 2076 else: 2077 raise FeatureLibError( 2078 "Expected an integer or floating-point number", self.cur_token_location_ 2079 ) 2080 2081 def expect_stat_flags(self): 2082 value = 0 2083 flags = { 2084 "OlderSiblingFontAttribute": 1, 2085 "ElidableAxisValueName": 2, 2086 } 2087 while self.next_token_ != ";": 2088 if self.next_token_ in flags: 2089 name = self.expect_name_() 2090 value = value | flags[name] 2091 else: 2092 raise FeatureLibError( 2093 f"Unexpected STAT flag {self.cur_token_}", self.cur_token_location_ 2094 ) 2095 return value 2096 2097 def expect_stat_values_(self): 2098 if self.next_token_type_ == Lexer.FLOAT: 2099 return self.expect_float_() 2100 elif self.next_token_type_ is Lexer.NUMBER: 2101 return self.expect_number_() 2102 else: 2103 raise FeatureLibError( 2104 "Expected an integer or floating-point number", self.cur_token_location_ 2105 ) 2106 2107 def expect_string_(self): 2108 self.advance_lexer_() 2109 if self.cur_token_type_ is Lexer.STRING: 2110 return self.cur_token_ 2111 raise FeatureLibError("Expected a string", self.cur_token_location_) 2112 2113 def advance_lexer_(self, comments=False): 2114 if comments and self.cur_comments_: 2115 self.cur_token_type_ = Lexer.COMMENT 2116 self.cur_token_, self.cur_token_location_ = self.cur_comments_.pop(0) 2117 return 2118 else: 2119 self.cur_token_type_, self.cur_token_, self.cur_token_location_ = ( 2120 self.next_token_type_, 2121 self.next_token_, 2122 self.next_token_location_, 2123 ) 2124 while True: 2125 try: 2126 ( 2127 self.next_token_type_, 2128 self.next_token_, 2129 self.next_token_location_, 2130 ) = next(self.lexer_) 2131 except StopIteration: 2132 self.next_token_type_, self.next_token_ = (None, None) 2133 if self.next_token_type_ != Lexer.COMMENT: 2134 break 2135 self.cur_comments_.append((self.next_token_, self.next_token_location_)) 2136 2137 @staticmethod 2138 def reverse_string_(s): 2139 """'abc' --> 'cba'""" 2140 return "".join(reversed(list(s))) 2141 2142 def make_cid_range_(self, location, start, limit): 2143 """(location, 999, 1001) --> ["cid00999", "cid01000", "cid01001"]""" 2144 result = list() 2145 if start > limit: 2146 raise FeatureLibError( 2147 "Bad range: start should be less than limit", location 2148 ) 2149 for cid in range(start, limit + 1): 2150 result.append("cid%05d" % cid) 2151 return result 2152 2153 def make_glyph_range_(self, location, start, limit): 2154 """(location, "a.sc", "d.sc") --> ["a.sc", "b.sc", "c.sc", "d.sc"]""" 2155 result = list() 2156 if len(start) != len(limit): 2157 raise FeatureLibError( 2158 'Bad range: "%s" and "%s" should have the same length' % (start, limit), 2159 location, 2160 ) 2161 2162 rev = self.reverse_string_ 2163 prefix = os.path.commonprefix([start, limit]) 2164 suffix = rev(os.path.commonprefix([rev(start), rev(limit)])) 2165 if len(suffix) > 0: 2166 start_range = start[len(prefix) : -len(suffix)] 2167 limit_range = limit[len(prefix) : -len(suffix)] 2168 else: 2169 start_range = start[len(prefix) :] 2170 limit_range = limit[len(prefix) :] 2171 2172 if start_range >= limit_range: 2173 raise FeatureLibError( 2174 "Start of range must be smaller than its end", location 2175 ) 2176 2177 uppercase = re.compile(r"^[A-Z]$") 2178 if uppercase.match(start_range) and uppercase.match(limit_range): 2179 for c in range(ord(start_range), ord(limit_range) + 1): 2180 result.append("%s%c%s" % (prefix, c, suffix)) 2181 return result 2182 2183 lowercase = re.compile(r"^[a-z]$") 2184 if lowercase.match(start_range) and lowercase.match(limit_range): 2185 for c in range(ord(start_range), ord(limit_range) + 1): 2186 result.append("%s%c%s" % (prefix, c, suffix)) 2187 return result 2188 2189 digits = re.compile(r"^[0-9]{1,3}$") 2190 if digits.match(start_range) and digits.match(limit_range): 2191 for i in range(int(start_range, 10), int(limit_range, 10) + 1): 2192 number = ("000" + str(i))[-len(start_range) :] 2193 result.append("%s%s%s" % (prefix, number, suffix)) 2194 return result 2195 2196 raise FeatureLibError('Bad range: "%s-%s"' % (start, limit), location) 2197 2198 2199class SymbolTable(object): 2200 def __init__(self): 2201 self.scopes_ = [{}] 2202 2203 def enter_scope(self): 2204 self.scopes_.append({}) 2205 2206 def exit_scope(self): 2207 self.scopes_.pop() 2208 2209 def define(self, name, item): 2210 self.scopes_[-1][name] = item 2211 2212 def resolve(self, name): 2213 for scope in reversed(self.scopes_): 2214 item = scope.get(name) 2215 if item: 2216 return item 2217 return None 2218